Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76794be79b | ||
|
|
730c35aa0a | ||
|
|
c0190e1336 | ||
|
|
bda377eb16 | ||
|
|
774e221f8b | ||
|
|
87d241c15a | ||
|
|
fd6cf968dc |
No files matched your search
@@ -97,7 +97,7 @@
|
||||
- **反向**:diff 里每个行为改动至少一行;没有对应行的改动,必须写明「非行为改动」或「由第 X 行覆盖」。
|
||||
- **测试侧**:新增或改写的每条断言至少挂一行;挂不上的孤儿断言删掉或补挂到需求点。
|
||||
|
||||
**验证集清单冻结与能力清单维护锚(2026-10-28 用户裁,验证与清单体系整改阶段 1)**:`game/tests/验证集清单.md` 自 2026-10-28 起**整册冻结**——不再随断言新增、删除或改判同步;它只作阶段 2「存量断言精简」的对账基准(基点 `b9260138`/1587 条),阶段 2 精简完成后删除。覆盖索引改由 `game/tests/README.md`「能力清单 v1」承担:每行一个能力,给真实入口与 2–3 个代表用例(`文件:函数`)。维护义务随之转移——**每批收口核对本批受影响能力行**:代表用例可 grep、在本批真实跑批中执行、真实入口未绕过改动点,并刷新「最后验证批次」;新增能力加行,能力退役删行。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。
|
||||
**能力清单维护锚(2026-10-28 用户裁,验证与清单体系整改阶段 1;阶段 2 后定稿)**:逐断言账本 `game/tests/验证集清单.md` 已随阶段 2「存量断言精简」完成退役——冻结对账基准(基点 `b9260138`/1587 条)与精简对账结果(33 站点并为 7、1587→1561,合回 `87d241c1`)留 git 历史。覆盖索引由 `game/tests/README.md`「能力清单 v1」唯一承担:每行一个能力,给真实入口与 2–3 个代表用例(`文件:函数`)。维护义务——**每批收口核对本批受影响能力行**:代表用例可 grep、在本批真实跑批中执行、真实入口未绕过改动点,并刷新「最后验证批次」;新增能力加行,能力退役删行。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。
|
||||
|
||||
### 8.4 真实入口断言(验收闭合的主判据)
|
||||
|
||||
|
||||
+13
-12
@@ -30,10 +30,7 @@ var _rap := 0
|
||||
var rap: int:
|
||||
get: return _rap
|
||||
var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
|
||||
var last_round_end: Dictionary[Variant, Variant]:
|
||||
get: return _last_round_end
|
||||
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
|
||||
var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图)
|
||||
var _pending_ap_delta := 0 # 下轮行动点修正排队(action 型 ±1 排队,轮开始兑现后清零)
|
||||
# 蓄力流运行态:转蓄力=round_end grant_ap(mode:transfer) 订阅(finish_round_stop 定夺)、
|
||||
# 深挖对价=dig_paid counter_spend 订阅(request_dig 施加面);charge_layers=蓄力层账本
|
||||
@@ -44,7 +41,6 @@ var charge_layers: int:
|
||||
var _fx: EffectEngine = null
|
||||
var _ctx: EffectContext = null
|
||||
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺)
|
||||
var _ap_exhausted_last := false # 上轮 AP 耗尽封印(一滴不剩 round_start when 面)
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||||
@@ -66,7 +62,7 @@ func _phase_text(p: int) -> String:
|
||||
|
||||
|
||||
func _cost(kind: String) -> int:
|
||||
var ae = db.action_economy
|
||||
var ae = db.run.action_economy
|
||||
match kind:
|
||||
"dig":
|
||||
return ae.dig_ap_cost # 基础挖格成本恒表值(钻头对价挂付费开掘侧)
|
||||
@@ -204,9 +200,9 @@ func request_refresh() -> bool:
|
||||
if _phase != Phase.ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
if _rap < db.action_economy.rap_refresh_cost:
|
||||
if _rap < db.run.action_economy.rap_refresh_cost:
|
||||
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
|
||||
_rap -= db.action_economy.rap_refresh_cost
|
||||
_rap -= db.run.action_economy.rap_refresh_cost
|
||||
return true
|
||||
|
||||
|
||||
@@ -216,7 +212,6 @@ func notify_settlement_done(next_round_mode: String) -> bool:
|
||||
_reject("phase")
|
||||
return false
|
||||
_settlement_consumed = true
|
||||
_next_round_mode = next_round_mode
|
||||
return true
|
||||
|
||||
|
||||
@@ -249,7 +244,7 @@ func notify_round_start() -> bool:
|
||||
else:
|
||||
_reject("phase")
|
||||
return false
|
||||
_ap = maxi(0, db.action_economy.ap_per_round + _pending_ap_delta) # 每轮基线 + 排队修正(下限 0)
|
||||
_ap = maxi(0, db.run.action_economy.ap_per_round + _pending_ap_delta) # 每轮基线 + 排队修正(下限 0)
|
||||
_pending_ap_delta = 0
|
||||
_phase = Phase.ACTIVE
|
||||
_settlement_consumed = false
|
||||
@@ -272,13 +267,19 @@ func _finish_round(path_tag: String, continuation_hint: String) -> void:
|
||||
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
|
||||
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
|
||||
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
|
||||
_ap_exhausted_last = path_tag == "ap_exhausted" # 一滴不剩 when 面(轮始发射读数)
|
||||
_phase = Phase.WAITING
|
||||
|
||||
|
||||
# 上轮 AP 是否耗尽(round_start 发射 payload 读数面;轮始兑现后保持到本轮末)
|
||||
# 上轮组包读数({path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源;
|
||||
# 属性→方法 K5 换形,消费方 minesweeper_rules 单点改址)
|
||||
func last_round_end() -> Dictionary[String, String]:
|
||||
return _last_round_end
|
||||
|
||||
|
||||
# 上轮 AP 是否耗尽(round_start 发射 payload 读数面;轮始兑现后保持到本轮末)——
|
||||
# 派生读(同源同刻:_last_round_end 组包点即派生原点,K5 派生合并——存储成员退役)
|
||||
func ap_exhausted_last_round() -> bool:
|
||||
return _ap_exhausted_last
|
||||
return _last_round_end.get("path_tag", "") == "ap_exhausted"
|
||||
|
||||
|
||||
func _reject(source: String) -> void:
|
||||
|
||||
@@ -105,7 +105,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_stats.armor_type = String(params.get("armor_type", ""))
|
||||
summary = {}
|
||||
_reset_curio_state()
|
||||
levels_total = db.level_count() # 表访问收口(关数读 level 表行数)
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动)
|
||||
@@ -120,7 +120,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
|
||||
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
|
||||
for iid_v2 in _attunement:
|
||||
var rp_row: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v2))
|
||||
var rp_row: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v2))
|
||||
if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"):
|
||||
_refresh_bank.add_refresh_stock(int(rp_row.get("magnitude", 0)))
|
||||
# 特性开局注入(MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
|
||||
@@ -184,7 +184,7 @@ func _resume_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||||
_wallet.restore_fragment(bp, params)
|
||||
_journal.restore_fragment(bp, params)
|
||||
summary = {}
|
||||
levels_total = db.level_count() # 表访问收口(关数读 level 表行数)
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_pending_next_level = false
|
||||
|
||||
@@ -223,7 +223,7 @@ func _resume_params(bp: Dictionary, params: Dictionary) -> void:
|
||||
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
|
||||
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
|
||||
_stats.clamp_hp_to_resume_cap(int(db.hero.max_hp))
|
||||
_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
|
||||
@@ -330,7 +330,7 @@ func present_node_curio(candidates: Array) -> void:
|
||||
func _attune_refresh_x_total() -> int:
|
||||
var total := 0
|
||||
for iid_v in _attunement:
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||||
if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"):
|
||||
total += int(arow.get("magnitude", 0))
|
||||
return total
|
||||
@@ -369,8 +369,8 @@ func _loadout_on_level_inventory() -> void:
|
||||
func _boss_keystone_drop(li: int) -> void:
|
||||
var first := true
|
||||
if _save_manager != null:
|
||||
first = not _save_manager.boss_first_kill_marked(li)
|
||||
var row: Dictionary[Variant, Variant] = db.meta_boss_keystone_row(li)
|
||||
first = not _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}) # 逐杀明细+累计单写原语
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func item_ids() -> Array:
|
||||
func seed_for_adventure(inventory: TreasureItems) -> void:
|
||||
for iid_v in _ids:
|
||||
var iid := String(iid_v)
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(iid)
|
||||
if String(arow.get("form", "")) != "active_charged":
|
||||
continue
|
||||
_ensure_rows(iid, maxi(0, int(arow.get("charges_per_adventure", 0))), inventory)
|
||||
@@ -44,7 +44,7 @@ func seed_for_adventure(inventory: TreasureItems) -> void:
|
||||
func reset_cycle(cycle: String, inventory: TreasureItems) -> void:
|
||||
for iid_v in _ids:
|
||||
var iid := String(iid_v)
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(iid)
|
||||
if String(arow.get("form", "")) != "active_charged":
|
||||
continue
|
||||
if String(arow.get("charge_reset", "adventure")) != cycle:
|
||||
@@ -94,7 +94,7 @@ func breakpoint_fragment(inventory: TreasureItems) -> Dictionary[Variant, Varian
|
||||
var charges: Dictionary[Variant, Variant] = {}
|
||||
for iid_v in _ids:
|
||||
var iid := String(iid_v)
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(iid)
|
||||
if String(arow.get("form", "")) == "active_charged":
|
||||
charges[iid] = charges_left(iid, inventory)
|
||||
return {"attunement_charges": charges}
|
||||
|
||||
@@ -12,6 +12,6 @@ static func slots(db: ConfigDb, save: Variant) -> int:
|
||||
return 0
|
||||
var n := 0
|
||||
for i in range(1, 4):
|
||||
if save.tree_level("att_socket%d" % i) >= 1:
|
||||
if save.meta_state.tree_state().nodes.get("att_socket%d" % i, 0) >= 1:
|
||||
n += 1
|
||||
return n
|
||||
@@ -175,5 +175,5 @@ func _fx_fact(fact: String) -> Variant:
|
||||
# 基线 = enc 口径,-1(表值兜底态)时解析为表值后叠加,守恒口径与 begin_battle 注入一致)
|
||||
func _sync_hero_ac() -> void:
|
||||
if int(ac.base) < 0:
|
||||
ac.base = int(db.hero.armor_class)
|
||||
ac.base = int(db.combat.hero.armor_class)
|
||||
damage.set_hero_ac_override(int(ac.base) + int(ac.round) + int(ac.battle))
|
||||
@@ -180,7 +180,7 @@ func attune_has(item_id: String) -> bool:
|
||||
func attune_param_sum(kind: String) -> int:
|
||||
var total := 0
|
||||
for iid_v in attunement:
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||||
if (arow.get("effect_kinds", []) as Array).has(kind):
|
||||
total += int(arow.get("magnitude", 0))
|
||||
return total
|
||||
@@ -190,7 +190,7 @@ func attune_param_sum(kind: String) -> int:
|
||||
# 首行语义=装配集内至多一件积点制〔试炼镐〕)
|
||||
func _attune_charge_safe_row() -> Dictionary:
|
||||
for iid_v in attunement:
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||||
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
|
||||
return arow
|
||||
return {}
|
||||
@@ -301,10 +301,8 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
|
||||
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))])
|
||||
return false
|
||||
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
|
||||
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
|
||||
if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), dice[0],
|
||||
_hero_atk, int(enc.get("hero_max_hp", -1)), dice[1],
|
||||
String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), String(enc.get("node_type", "battle"))): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
|
||||
_hero_atk = int(enc.get("hero_atk", int(db.combat.hero.atk)))
|
||||
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk, "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
|
||||
_fatal("敌人系统 spawn 被拒")
|
||||
return false
|
||||
return true
|
||||
@@ -317,7 +315,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
|
||||
# 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。
|
||||
func _begin_battle_dice(enc: Dictionary) -> Array:
|
||||
# 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type)
|
||||
var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle")))
|
||||
var erowset_pre: Array[Variant] = db.combat.enemy.resolve_rowset(int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle")))
|
||||
if erowset_pre.is_empty():
|
||||
return []
|
||||
var needs_count := false
|
||||
@@ -462,7 +460,7 @@ func _install_new_board(during_round := false) -> bool:
|
||||
func _resolve_global_variant(board: BoardData) -> void:
|
||||
board.board_variants.clear()
|
||||
_reward_economy.board_skew = 0
|
||||
var row: Dictionary[Variant, Variant] = db.global_variant_row(int(encounter.level_index), String(board.board_type),
|
||||
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
|
||||
String(encounter.get("node_type", "")))
|
||||
if row.is_empty():
|
||||
return
|
||||
@@ -482,7 +480,7 @@ func _resolve_global_variant(board: BoardData) -> void:
|
||||
func _place_variants(board: BoardData) -> void:
|
||||
if not board.board_variants.is_empty():
|
||||
return
|
||||
var pool: Array[Variant] = db.variant_pool(int(encounter.level_index), String(board.board_type))
|
||||
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
|
||||
if pool.is_empty():
|
||||
return
|
||||
var r0 := {}
|
||||
@@ -496,8 +494,8 @@ func _place_variants(board: BoardData) -> void:
|
||||
return
|
||||
var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次)
|
||||
var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)]
|
||||
var total: int = int(round(float(candidates.size()) * float(db.variation.percent_of_cells) / 100.0)) \
|
||||
+ (int(encounter.level_index) - 1) * int(db.variation.per_level_bonus)
|
||||
var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \
|
||||
+ (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus)
|
||||
total = mini(total, candidates.size())
|
||||
_place_variants_apply(board, candidates, stream, vrow, total)
|
||||
|
||||
@@ -506,7 +504,7 @@ func _place_variants(board: BoardData) -> void:
|
||||
func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void:
|
||||
var placed := {}
|
||||
while placed.size() < total:
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.variation.pack_max)))
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max)))
|
||||
var free: Array[Vector2i] = []
|
||||
for c in candidates:
|
||||
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
|
||||
@@ -638,7 +636,7 @@ func _harmful_variants(vids: Array) -> Array:
|
||||
var out: Array[Variant] = []
|
||||
for vid_v in vids:
|
||||
var vid := String(vid_v)
|
||||
var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(vid)
|
||||
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(vid)
|
||||
if String(vrow.get("effect", "")) == "flat_damage":
|
||||
out.append(vid)
|
||||
return out
|
||||
@@ -677,12 +675,12 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
|
||||
# 摇定后、兑现前死亡 → 本拍技能作废。
|
||||
func _roll_round_actions() -> void:
|
||||
var pity_m: int = int(db.skill.pity_threshold)
|
||||
var pity_m: int = int(db.combat.skill.pity_threshold)
|
||||
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
|
||||
# 容器内直读主角行)
|
||||
var facts := {
|
||||
"round_index": round_index,
|
||||
"last_round_end": "none" if (action_economy.last_round_end as Dictionary).is_empty() else String((action_economy.last_round_end as Dictionary).get("path_tag", "none")),
|
||||
"last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")),
|
||||
"mines_hit_last": int(_mines_hits["last"]),
|
||||
"mines_hit_total": int(_mines_hits["total"]),
|
||||
}
|
||||
@@ -692,7 +690,7 @@ func _roll_round_actions() -> void:
|
||||
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
|
||||
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
|
||||
for sid in _enemy_system.check_trigger_unlocks(u, facts):
|
||||
var srow_u: Dictionary[Variant, Variant] = db.skill_row(sid)
|
||||
var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
|
||||
_emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
|
||||
round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
|
||||
0, {"skill_id": sid})
|
||||
@@ -733,7 +731,7 @@ func _defender_ac(unit: Dictionary) -> int:
|
||||
|
||||
func _pool_effective(effect_id: String) -> int:
|
||||
var side := "hero"
|
||||
var row: Dictionary[Variant, Variant] = db.status_effect_row(effect_id)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id)
|
||||
if not row.is_empty():
|
||||
side = String(row.target_side)
|
||||
for eff in _status_effects.query_active(side):
|
||||
@@ -818,9 +816,9 @@ func _exit_distribution() -> void:
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
|
||||
func can_refresh() -> bool:
|
||||
return battle_result == "" \
|
||||
and _refresh_used_this_round < int(db.action_economy.refresh_cap_per_round) \
|
||||
and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \
|
||||
and action_economy.phase == action_economy.PHASE_ACTIVE \
|
||||
and action_economy.rap >= int(db.action_economy.rap_refresh_cost)
|
||||
and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost)
|
||||
|
||||
|
||||
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
|
||||
@@ -870,14 +868,14 @@ func _battle_over(result: String) -> void:
|
||||
# + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。
|
||||
func _roll_gp_drop() -> int:
|
||||
var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal"
|
||||
var drops: Dictionary[Variant, Variant] = db.shop_gp_drop_row(tier)
|
||||
var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier)
|
||||
var roll := 0
|
||||
if drops.is_empty():
|
||||
push_error("[战斗结算] gp_drops 缺行 tier=%s——掉落掷定按 0 兑付" % tier)
|
||||
else:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("gp_drop")
|
||||
roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0)))
|
||||
var drop := roll + db.shop_level_gp_bonus(int(encounter.get("level_index", 0)))
|
||||
var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0)))
|
||||
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
|
||||
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
|
||||
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
|
||||
@@ -899,14 +897,14 @@ func presentation_snapshot() -> Dictionary:
|
||||
# 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行)
|
||||
var enemy_effects: Array[Dictionary] = []
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
var erow: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
|
||||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
|
||||
if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]:
|
||||
enemy_effects.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)})
|
||||
for i in all.size():
|
||||
var u: Dictionary[Variant, Variant] = all[i]
|
||||
var alive: bool = bool(u.get("alive", false))
|
||||
var pending: String = String(u.get("pending_skill", "")) if alive else ""
|
||||
var pending_row: Dictionary[Variant, Variant] = db.skill_row(pending)
|
||||
var pending_row: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(pending)
|
||||
enemies.append({
|
||||
"order": i + 1,
|
||||
"display_name": String(u.display_name),
|
||||
|
||||
+128
-83
@@ -22,12 +22,19 @@ static func target_mine_count(params: Dictionary) -> int:
|
||||
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。
|
||||
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表
|
||||
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub)
|
||||
var row: Dictionary[Variant, Variant] = db.resolve_board_gen(L, D)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.board_gen.resolve_row(L, D)
|
||||
if row.is_empty():
|
||||
_fatal("BoardGen 行缺失 (%d,%d)" % [L, D])
|
||||
return null
|
||||
# 生成期表值单次读取快照(防热重载撕裂 attempt 语义)
|
||||
var params := {
|
||||
var params := _board_params(row, L, D)
|
||||
var outcome := _attempt_loop(params, rng_stream, db)
|
||||
_finalize(outcome["board"], params, outcome["attempts"], outcome["mode"], outcome["r0_rej"])
|
||||
return outcome["board"]
|
||||
|
||||
|
||||
## 生成期表值单次读取快照(防热重载撕裂 attempt 语义;零 RNG)
|
||||
func _board_params(row: Dictionary, L: int, D: int) -> Dictionary:
|
||||
return {
|
||||
"L": L, "D": D,
|
||||
"w": int(row.board_width), "h": int(row.board_height),
|
||||
"density": float(row.mine_density),
|
||||
@@ -41,10 +48,11 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
|
||||
"shave_r": int(row.get("shave_ring_radius", 1)),
|
||||
"r0_cap": int(row.get("r0_max_cap", 0)),
|
||||
}
|
||||
var best_legal: BoardData = null
|
||||
var best_legal_score := -1.0
|
||||
var best_bad: BoardData = null
|
||||
var best_badness := [999999, 999999, 999999]
|
||||
|
||||
|
||||
## 重试主循环:逐尝试评分择优,产出三分出口结果(board/mode/attempts/r0_rej)
|
||||
func _attempt_loop(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Dictionary: # type-hint: ignore(同 generate——测量面 stub 注入)
|
||||
var best := {"legal": null, "legal_score": -1.0, "bad": null, "badness": [999999, 999999, 999999]}
|
||||
var attempts_used := 0
|
||||
var accepted: BoardData = null
|
||||
var accept_mode := ""
|
||||
@@ -53,35 +61,37 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
|
||||
attempts_used = k
|
||||
var b := _attempt(params, rng_stream, db)
|
||||
var badness := _badness(b, params)
|
||||
if best_bad == null or _lex_less(badness, best_badness):
|
||||
best_bad = b
|
||||
best_badness = badness
|
||||
if best["bad"] == null or _lex_less(badness, best["badness"]):
|
||||
best["bad"] = b
|
||||
best["badness"] = badness
|
||||
# R₀≤cap 重掷闸触发计数(迭代14① T3——尝试事实,与合法性判定独立记录;占比=T5 闸触发率读数)
|
||||
if int(params.r0_cap) > 0 and b.compute_r0().size() > int(params.r0_cap):
|
||||
r0_cap_rejected += 1
|
||||
if legality(b, params):
|
||||
var m: Dictionary[Variant, Variant] = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次)
|
||||
var inf: float = m["inferability"]
|
||||
if best_legal == null or inf > best_legal_score:
|
||||
best_legal = b
|
||||
best_legal_score = inf
|
||||
var inf: float = measure_inferability(b)["inferability"] # 求解器每合法板恰一次(O3 收口——此前重复调用两次)
|
||||
if best["legal"] == null or inf > best["legal_score"]:
|
||||
best["legal"] = b
|
||||
best["legal_score"] = inf
|
||||
if inf >= params.target: # 单一判据(棋盘生成:现行规则已回归"可推理率 ≥ 及格线")
|
||||
accepted = b
|
||||
accept_mode = "first_pass"
|
||||
break
|
||||
if accepted == null:
|
||||
if best_legal != null:
|
||||
accepted = best_legal
|
||||
accept_mode = "accept_best"
|
||||
accepted.degraded_product = true # 降级标记(独立于 illegal_product——三分出口表要求标记随产物上抛)
|
||||
push_warning("[棋盘生成] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
|
||||
else:
|
||||
accepted = best_bad
|
||||
accept_mode = "accept_worst"
|
||||
accepted.illegal_product = true
|
||||
push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
|
||||
_finalize(accepted, params, attempts_used, accept_mode, db, r0_cap_rejected)
|
||||
return accepted
|
||||
var fallback := _accept_fallback(params, best["legal"], best["legal_score"], best["bad"])
|
||||
accepted = fallback[0]
|
||||
accept_mode = fallback[1]
|
||||
return {"board": accepted, "mode": accept_mode, "attempts": attempts_used, "r0_rej": r0_cap_rejected}
|
||||
|
||||
|
||||
## 三分出口(重试耗尽):接受最优(降级标记)或接受最不坏(非法产物标记——玩家永不卡死)
|
||||
func _accept_fallback(params: Dictionary, best_legal: BoardData, best_legal_score: float, best_bad: BoardData) -> Array:
|
||||
if best_legal != null:
|
||||
best_legal.degraded_product = true # 降级标记(独立于 illegal_product——三分出口表要求标记随产物上抛)
|
||||
push_warning("[棋盘生成] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
|
||||
return [best_legal, "accept_best"]
|
||||
best_bad.illegal_product = true
|
||||
push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
|
||||
return [best_bad, "accept_worst"]
|
||||
|
||||
|
||||
# ==================== 五阶段:单次尝试 ====================
|
||||
@@ -94,8 +104,20 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
b.board_type = str(params.board_type)
|
||||
b.level_index = params.L
|
||||
b.node_depth = params.D
|
||||
# ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"
|
||||
# (−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
|
||||
# ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"(_populate_mines 承载——Fisher-Yates 取号段整体随迁)
|
||||
var removed := _populate_mines(b, params, rng_stream)
|
||||
# ③ 变异布置(地形变异 框架面):布置约束与算法随首批变异类型定稿(生成期布置接口结构位),
|
||||
# 定稿前零布置——清单为空是合法态(P0),静默产出无变异图。
|
||||
b.metrics = {
|
||||
"metric_version": METRIC_VERSION,
|
||||
"ring_removed": removed,
|
||||
}
|
||||
return b
|
||||
|
||||
|
||||
## 布雷与剃雷域:域 = 全格 − 出生 3×3;均匀布雷(部分 Fisher-Yates,确定性,消费 board_gen 流)
|
||||
## 后剃出生环雷,返回 ring_removed(指标语义不变——现值变小=收窄批预期)
|
||||
func _populate_mines(b: BoardData, params: Dictionary, rng_stream: RandomNumberGenerator) -> int:
|
||||
var spawn := {}
|
||||
for c in b.spawn_cells():
|
||||
spawn[c] = true
|
||||
@@ -104,7 +126,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
if not spawn.has(c):
|
||||
domain.append(c)
|
||||
var n_mines: int = target_mine_count(params)
|
||||
# 部分 Fisher-Yates(确定性,消费 board_gen 流)
|
||||
# 部分 Fisher-Yates(确定性,消费 board_gen 流)——取号序冻结(契约:同种子逐位一致)
|
||||
for i in range(0, mini(n_mines, domain.size())):
|
||||
var j := rng_stream.randi_range(i, domain.size() - 1)
|
||||
var tmp = domain[i]
|
||||
@@ -116,9 +138,14 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
# 先布后删:剃雷域 = 距 spawn_center 切比雪夫距离 ≤1+r 的格(迭代14①收窄批 T2——原「R₀ flood 域∪
|
||||
# 外扩 1 环」全域剃雷=开局开阔地巨大根因〔10×10 板 R₀ 均值 68.6=板面 68.6%〕;收窄后开阔地=
|
||||
# 出生邻域外的自然 flood。r 读表 shave_ring_radius(r=1 → 5×5=25 格;r=0 即只剃出生 3×3)。
|
||||
# 只减不增、无迭代语义不变;ring_removed 指标语义不变(现值变小=预期);零 RNG 消费变化——
|
||||
# 同种子布雷序列逐位一致,仅移除域收窄。
|
||||
var shave_r: int = int(params.get("shave_r", 1))
|
||||
# 只减不增、无迭代语义不变;零 RNG 消费变化。
|
||||
var removed := _shave_ring(b, int(params.get("shave_r", 1)))
|
||||
_compute_digits(b)
|
||||
return removed
|
||||
|
||||
|
||||
## 剃雷环域(零 RNG):距出生心切比雪夫 ≤1+r 的环内剃雷,返回剃除数
|
||||
func _shave_ring(b: BoardData, shave_r: int) -> int:
|
||||
var ring := {}
|
||||
for c in b.all_cells():
|
||||
if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= 1 + shave_r:
|
||||
@@ -128,14 +155,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
if b.mines.has(c):
|
||||
b.mines.erase(c)
|
||||
removed += 1
|
||||
_compute_digits(b)
|
||||
# ③ 变异布置(地形变异 框架面):布置约束与算法随首批变异类型定稿(生成期布置接口结构位),
|
||||
# 定稿前零布置——清单为空是合法态(P0),静默产出无变异图。
|
||||
b.metrics = {
|
||||
"metric_version": METRIC_VERSION,
|
||||
"ring_removed": removed,
|
||||
}
|
||||
return b
|
||||
return removed
|
||||
|
||||
|
||||
func _compute_digits(b: BoardData) -> void:
|
||||
@@ -164,49 +184,12 @@ func measure_inferability(b: BoardData) -> Dictionary:
|
||||
while changed:
|
||||
changed = false
|
||||
# 算子① 连锁:0 格的 8 邻安全格全入 R
|
||||
var frontier: Array[Variant] = r.keys()
|
||||
for x in frontier:
|
||||
if safe_set.has(x) and b.digit_of(x) == 0:
|
||||
for n in b.neighbors8(x):
|
||||
if safe_set.has(n) and not r.has(n):
|
||||
r[n] = true
|
||||
changed = true
|
||||
changed = _apply_chain_rule(b, r, safe_set) or changed
|
||||
# 信息源:数字约束 single + subset(solver_rules=single_subset 冻结域)
|
||||
var constraints := _build_constraints(b, r, known_mines, safe_set)
|
||||
# single 规则
|
||||
for con in constraints:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if not r.has(c):
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
for c in con.unknown:
|
||||
if not known_mines.has(c):
|
||||
known_mines[c] = true
|
||||
changed = true
|
||||
# subset 规则:U1 ⊂ U2 ⇒ (d2−d1, U2∖U1)
|
||||
var derived := []
|
||||
for i in constraints.size():
|
||||
for j in range(i + 1, constraints.size()):
|
||||
var a: Dictionary[Variant, Variant] = constraints[i]
|
||||
var bb: Dictionary[Variant, Variant] = constraints[j]
|
||||
var sub := _subset_of(a.unknown, bb.unknown)
|
||||
if sub == 1:
|
||||
derived.append(_diff_constraint(a, bb))
|
||||
elif sub == 2:
|
||||
derived.append(_diff_constraint(bb, a))
|
||||
for con in derived:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if not r.has(c): # 同 no-peek 口径:推导结论不再用 safe_set 复核(约束本身可靠)
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
for c in con.unknown:
|
||||
if not known_mines.has(c):
|
||||
known_mines[c] = true
|
||||
changed = true
|
||||
changed = _apply_single_rules(constraints, r, known_mines) or changed
|
||||
var derived := _derived_constraints(constraints)
|
||||
changed = _apply_derived_rules(derived, r, known_mines) or changed
|
||||
var r_inf := r.keys()
|
||||
var s_count := safe_set.size()
|
||||
var inferability: float = float(r_inf.size()) / float(maxi(1, s_count))
|
||||
@@ -217,6 +200,68 @@ func measure_inferability(b: BoardData) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
## 算子① 连锁域:0 格的 8 邻安全格全入 R(返回本轮是否有新增)
|
||||
func _apply_chain_rule(b: BoardData, r: Dictionary, safe_set: Dictionary) -> bool:
|
||||
var changed := false
|
||||
var frontier: Array[Variant] = r.keys()
|
||||
for x in frontier:
|
||||
if safe_set.has(x) and b.digit_of(x) == 0:
|
||||
for n in b.neighbors8(x):
|
||||
if safe_set.has(n) and not r.has(n):
|
||||
r[n] = true
|
||||
changed = true
|
||||
return changed
|
||||
|
||||
|
||||
## single 规则域:零雷约束开格/满雷约束标雷(返回本轮是否有新增)
|
||||
func _apply_single_rules(constraints: Array, r: Dictionary, known_mines: Dictionary) -> bool:
|
||||
var changed := false
|
||||
for con in constraints:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if not r.has(c):
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
for c in con.unknown:
|
||||
if not known_mines.has(c):
|
||||
known_mines[c] = true
|
||||
changed = true
|
||||
return changed
|
||||
|
||||
|
||||
## subset 派生域:U1 ⊂ U2 ⇒ (d2−d1, U2∖U1)(对约束对做包含判定,产出派生约束集)
|
||||
func _derived_constraints(constraints: Array) -> Array:
|
||||
var derived := []
|
||||
for i in constraints.size():
|
||||
for j in range(i + 1, constraints.size()):
|
||||
var a: Dictionary[Variant, Variant] = constraints[i]
|
||||
var bb: Dictionary[Variant, Variant] = constraints[j]
|
||||
var sub := _subset_of(a.unknown, bb.unknown)
|
||||
if sub == 1:
|
||||
derived.append(_diff_constraint(a, bb))
|
||||
elif sub == 2:
|
||||
derived.append(_diff_constraint(bb, a))
|
||||
return derived
|
||||
|
||||
|
||||
## 派生规则域:派生约束的零雷开格/满雷标雷(同 no-peek 口径:推导结论不再用 safe_set 复核——约束本身可靠)
|
||||
func _apply_derived_rules(derived: Array, r: Dictionary, known_mines: Dictionary) -> bool:
|
||||
var changed := false
|
||||
for con in derived:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if not r.has(c):
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
for c in con.unknown:
|
||||
if not known_mines.has(c):
|
||||
known_mines[c] = true
|
||||
changed = true
|
||||
return changed
|
||||
|
||||
|
||||
func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, safe_set: Dictionary) -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for c in r.keys():
|
||||
@@ -337,7 +382,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
|
||||
|
||||
# ==================== 产出上报(生成期指标集) ====================
|
||||
|
||||
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void: # type-hint: ignore(同 generate——测量面 stub 注入)
|
||||
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, r0_cap_rejected: int = 0) -> void:
|
||||
var m := measure_inferability(b)
|
||||
# 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命
|
||||
var n_target: int = target_mine_count(params)
|
||||
|
||||
+39
-319
@@ -1,6 +1,8 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 数值表单点访问入口:全部 .tres 表的加载、校验诊断与类型化访问器(垂直切片验证:改表即改玩法,数值零硬编码)。
|
||||
## 玩法代码禁止绕过本单点自建 load();消费面一律走访问器,禁直读行集。
|
||||
## 数值表单点访问入口:全部 .tres 表的加载、校验诊断与门面(垂直切片验证:改表即改玩法,数值零硬编码)。
|
||||
## 玩法代码禁止绕过本单点自建 load();行查询归各表类(表管自己的行——2026-10-27 全清设计定稿),
|
||||
## 表持有按域收进三容器(combat 战斗编排域/run 冒险 runner 域/crossrun 跨局元进度域),
|
||||
## 消费面经 db.<容器>.<表> 直连。「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88)。
|
||||
## headless --script 模式无 autoload——冒烟脚本显式实例化本类;加载失败 is_ready=false 加 fatal 封印,调用方须查。
|
||||
class_name ConfigDb
|
||||
extends Node
|
||||
@@ -29,39 +31,23 @@ const TABLE_MANIFEST := {
|
||||
"forge": "forge.tres", # ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面
|
||||
}
|
||||
|
||||
var action_economy: ActionEconomyTable
|
||||
var variation: VariationTable
|
||||
var status_effects: StatusEffectTable
|
||||
var board_gen: BoardGenTable
|
||||
var enemy: EnemyTable
|
||||
var skill: SkillTable
|
||||
var hero: HeroTable
|
||||
var reward: RewardTable
|
||||
var settlement: SettlementTable
|
||||
var level: LevelTable
|
||||
var difficulty: DifficultyTable
|
||||
var level_gen_params: LevelGenParams # 参数档类型化持用(生成器只吃参数档、不认关卡号——卡① 契约 6)
|
||||
# ShopTable 类型化持用(schema class_name 已已入库;
|
||||
# 消费面一律走下方 shop_* 访问器,禁直读行集)
|
||||
var shop: ShopTable = null
|
||||
# MetaTable 类型化持用(schema 与表同源已入库)
|
||||
var meta: MetaTable
|
||||
var meta_tree: MetaTreeTable # 类型化持用(消费面走 tree_node 访问器)
|
||||
var treasure_items: TreasureItemTable # 类型化持用(消费面走 treasure_* 访问器)
|
||||
var adventure: AdventureTable
|
||||
var feats: FeatTable # 类型化持用(消费面走 feat_* 访问器)
|
||||
var forge: ForgeTable # 锻造工坊配置(默认两轴+六路线行族——消费面走 forge_* 访问器)
|
||||
var combat: ConfigDomainCombat # 战斗编排域七表(board_gen/variation/status_effects/enemy/skill/hero/settlement)
|
||||
var run: ConfigDomainRun # 冒险 runner 域七表(level/difficulty/level_gen_params/reward/adventure/action_economy/shop)
|
||||
var crossrun: ConfigDomainCrossrun # 跨局元进度域五表(meta/meta_tree/treasure_items/feats/forge)
|
||||
|
||||
var is_ready := false
|
||||
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
|
||||
var _resolver: ConfigDbResolver # R1 难度行解析协作类(门面公有签名不变)
|
||||
var _resolver: ConfigDbResolver # R1 难度行解析协作类(门面公有签名不变;load_tables 装配段注入三表)
|
||||
var _validator := ConfigDbCrossValidator.new() # 跨表校验协作类(无状态——声明期即建)
|
||||
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
combat = ConfigDomainCombat.new()
|
||||
run = ConfigDomainRun.new()
|
||||
crossrun = ConfigDomainCrossrun.new()
|
||||
_resolver = ConfigDbResolver.new(push_diag,
|
||||
func() -> Variant: return board_gen.rows, func() -> Variant: return enemy.rows, func() -> Variant: return difficulty) # 协作类持门面引用(诊断/致命通道回呼)
|
||||
func() -> Variant: return combat.board_gen.rows, func() -> Variant: return combat.enemy.rows, func() -> Variant: return run.difficulty) # 协作类持门面引用(诊断/致命通道回呼)
|
||||
|
||||
|
||||
func push_diag(severity: String, category: String, message: String) -> void:
|
||||
@@ -132,312 +118,46 @@ func load_tables(root: String, collect_only: bool) -> bool:
|
||||
if has_errors and not collect_only:
|
||||
# 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建
|
||||
return false
|
||||
action_economy = loaded.get("action_economy")
|
||||
variation = loaded.get("variation")
|
||||
status_effects = loaded.get("status_effects")
|
||||
board_gen = loaded.get("board_gen")
|
||||
enemy = loaded.get("enemy")
|
||||
skill = loaded.get("skill")
|
||||
hero = loaded.get("hero")
|
||||
reward = loaded.get("reward")
|
||||
settlement = loaded.get("settlement")
|
||||
level = loaded.get("level")
|
||||
difficulty = loaded.get("difficulty")
|
||||
level_gen_params = loaded.get("level_gen_params")
|
||||
shop = loaded.get("shop")
|
||||
meta = loaded.get("meta")
|
||||
treasure_items = loaded.get("treasure_items")
|
||||
adventure = loaded.get("adventure")
|
||||
feats = loaded.get("feats")
|
||||
forge = loaded.get("forge")
|
||||
meta_tree = loaded.get("meta_tree")
|
||||
# 容器装配(19 表成员按域收进三容器——2026-10-27 全清设计定稿)
|
||||
combat.board_gen = loaded.get("board_gen")
|
||||
combat.variation = loaded.get("variation")
|
||||
combat.status_effects = loaded.get("status_effects")
|
||||
combat.enemy = loaded.get("enemy")
|
||||
combat.skill = loaded.get("skill")
|
||||
combat.hero = loaded.get("hero")
|
||||
combat.settlement = loaded.get("settlement")
|
||||
run.level = loaded.get("level")
|
||||
run.difficulty = loaded.get("difficulty")
|
||||
run.level_gen_params = loaded.get("level_gen_params")
|
||||
run.reward = loaded.get("reward")
|
||||
run.adventure = loaded.get("adventure")
|
||||
run.action_economy = loaded.get("action_economy")
|
||||
run.shop = loaded.get("shop")
|
||||
crossrun.meta = loaded.get("meta")
|
||||
crossrun.meta_tree = loaded.get("meta_tree")
|
||||
crossrun.treasure_items = loaded.get("treasure_items")
|
||||
crossrun.feats = loaded.get("feats")
|
||||
crossrun.forge = loaded.get("forge")
|
||||
# resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移)
|
||||
combat.board_gen._resolver = _resolver
|
||||
combat.enemy._resolver = _resolver
|
||||
run.difficulty._resolver = _resolver
|
||||
is_ready = not has_errors
|
||||
return not has_errors
|
||||
|
||||
|
||||
# ---- relic 访问器(消费面一律走本组,禁直读行集)----
|
||||
|
||||
func feat_row(feat_id: String) -> Dictionary:
|
||||
return feats.row_by_id(feat_id) if feats != null else {}
|
||||
|
||||
|
||||
# 规则改写型特性带令牌构建源(池接线——CurioDeck.build 消费;行 rarity→投放档映射)
|
||||
func feat_rows() -> Array:
|
||||
return feats.rows if feats != null else []
|
||||
|
||||
|
||||
# 天赋树节点行(MetaProgression 树段/营地树 UI 取数口)
|
||||
func tree_node(node_id: String) -> Dictionary:
|
||||
return meta_tree.row_by_id(node_id) if meta_tree != null else {}
|
||||
|
||||
|
||||
func tree_rows() -> Array:
|
||||
return meta_tree.rows if meta_tree != null else []
|
||||
|
||||
|
||||
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体:
|
||||
# ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名不变。
|
||||
func _cross_validate(loaded: Dictionary) -> void:
|
||||
_validator.cross_validate(loaded, push_diag)
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类)
|
||||
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
|
||||
# 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类;门面私有——
|
||||
# 消费面经门面者改 _ 前缀直调,cross_validator 直验入口公开面不变)
|
||||
func _validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
|
||||
return _validator.validate_skill_pool_links(skill_table, enemy_table, status_table)
|
||||
|
||||
|
||||
# ---- R1 难度行解析(ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)----
|
||||
# 解析本体:ConfigDbResolver(core/config_db_resolver.gd)承载——
|
||||
# 含 _resolve_progress/_ceil_key 与 R1 告警去重注册表;门面同名方法委托,越域 Fatal 语义一致。
|
||||
func resolve_board_gen(L: int, D: int) -> Dictionary:
|
||||
return _resolver.resolve_board_gen(L, D)
|
||||
|
||||
|
||||
func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
|
||||
return _resolver.resolve_enemy_rowset(L, D, row_kind) # 行集数组形态(非单行 Dictionary;D 维带寻址)
|
||||
|
||||
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
return _resolver.resolve_difficulty_modifiers(L, D)
|
||||
|
||||
|
||||
# ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;字段名对齐
|
||||
# board_type 词表,无 biome_tags)----
|
||||
# 门控第三维 = 节点身份(全局作用域变异):scope=global 行仅 boss/elite 节点入池,
|
||||
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
|
||||
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
|
||||
|
||||
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
|
||||
var pool: Array[Dictionary] = []
|
||||
for row in variation.rows:
|
||||
if String(row.get("scope", "region")) == "global" \
|
||||
and node_type != "boss" and node_type != "elite":
|
||||
continue
|
||||
if int(row.get("unlock_level", 999)) > level_index:
|
||||
continue
|
||||
var tags: Array[Variant] = row.get("board_types", [])
|
||||
if tags.is_empty():
|
||||
pool.append(row)
|
||||
continue
|
||||
if tags.has(board_type):
|
||||
pool.append(row)
|
||||
return pool
|
||||
|
||||
|
||||
# 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
|
||||
# boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
|
||||
# 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
|
||||
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
|
||||
if node_type != "boss" and node_type != "elite":
|
||||
return {}
|
||||
for row in variant_pool(level_index, board_type, node_type):
|
||||
if String(row.get("scope", "region")) == "global":
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func status_effect_row(effect_id: String) -> Dictionary:
|
||||
return status_effects.row_by_id(effect_id)
|
||||
|
||||
|
||||
# ---- 关卡行集访问器(表访问收口——level_runner/meta_progression/adventure_runner
|
||||
# 不直扫 db.level.rows;消费面一律走本访问器,行集解析单点归 LevelTable)----
|
||||
|
||||
# 关卡行单点读取(键 = level_index,缺行返回空字典——消费方按既有空行防御路径处理)
|
||||
func level_row(level_index: int) -> Dictionary:
|
||||
return level.row_by_level_index(level_index) if level != null else {}
|
||||
|
||||
|
||||
# 参数档单点读取(节点图生成器核心批 卡①——键 = level_index,缺行返回空字典;调用方据此产图,生成器本体不读关卡号)
|
||||
func level_gen_params_row(level_index: int) -> Dictionary:
|
||||
return level_gen_params.row_by_level_index(level_index) if level_gen_params != null else {}
|
||||
|
||||
|
||||
# 序列总关数(AdventureRunner levels_total / MetaProgression 关卡完成累加域的取数口径)
|
||||
func level_count() -> int:
|
||||
return level.rows.size() if level != null else 0
|
||||
|
||||
|
||||
# ---- ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)----
|
||||
# ── 秘宝访问器(消费面一律走访问器,禁直读行集)──
|
||||
# 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径
|
||||
func treasure_item_row(item_id: String) -> Dictionary:
|
||||
return treasure_items.row_by_id(item_id) if treasure_items != null else {}
|
||||
|
||||
|
||||
# 过渡渠道活行(form 空 = 切片期过渡形态)——掉落投放/停点池/工坊解锁取数口径;同调行(form 非空)
|
||||
# 永不入过渡渠道
|
||||
func treasure_transitional_rows() -> Array:
|
||||
return treasure_items.transitional_rows() if treasure_items != null else []
|
||||
|
||||
|
||||
# 同调行集(form 非空——装配覆盖层/效果接线的取数口径)
|
||||
func treasure_attunement_rows() -> Array:
|
||||
return treasure_items.attunement_rows() if treasure_items != null else []
|
||||
|
||||
|
||||
# 活行词表(掉落投放 effect_id 白名单——过渡渠道口径)
|
||||
# ── 同调槽位数=树同调位节点点亮数,判定源=Attunement.slots(无 attunement_growth 谓词表访问器);
|
||||
# boss_kill_count/clear_count 计数域保留——非槽位硬消费在册 ──
|
||||
|
||||
|
||||
# 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} ·
|
||||
# level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value,
|
||||
# base_price, price_increment} · price_level_bonus {level_index, bonus}。
|
||||
# 行族经 ShopTable 同名数组属性读取(.get 容缺——表已入库前查询恒空,掉落按 0 兑付的兜底在调用方)。
|
||||
|
||||
func shop_gp_drop_row(tier: String) -> Dictionary:
|
||||
for row in _shop_rows("gp_drops"):
|
||||
if String(row.get("tier", "")) == tier:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func shop_level_gp_bonus(level_index: int) -> int:
|
||||
for row in _shop_rows("level_gp_bonus"):
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return int(row.get("bonus", 0))
|
||||
return 0
|
||||
|
||||
|
||||
func shop_price_level_bonus(level_index: int) -> int:
|
||||
for row in _shop_rows("price_level_bonus"):
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return int(row.get("bonus", 0))
|
||||
return 0
|
||||
|
||||
|
||||
func shop_products() -> Array:
|
||||
return _shop_rows("products")
|
||||
|
||||
|
||||
func shop_product_row(product_id: String) -> Dictionary:
|
||||
for row in _shop_rows("products"):
|
||||
if String(row.get("product_id", "")) == product_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# ── 共享池货架访问器(M-4——消费面一律走访问器,禁直读行集)──
|
||||
|
||||
# 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
|
||||
func shop_pool_rows() -> Array:
|
||||
return _shop_rows("pool_shelf")
|
||||
|
||||
|
||||
func shop_pool_row(kind_id: String) -> Dictionary:
|
||||
for row in _shop_rows("pool_shelf"):
|
||||
if String(row.get("kind_id", "")) == kind_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 乘法价格因子(provisional 投值——0 = 表未投值,消费方按 fail-fast 域校验拦截)
|
||||
func shop_price_a() -> float:
|
||||
return float(shop.price_a) if shop != null else 0.0
|
||||
|
||||
|
||||
func shop_price_b() -> float:
|
||||
return float(shop.price_b) if shop != null else 0.0
|
||||
|
||||
|
||||
# 行族数组读取(ShopTable 属性名 = 行族名;表未已入库/属性缺失 = 空集非错误态)
|
||||
func _shop_rows(family: String) -> Array:
|
||||
if shop == null:
|
||||
return []
|
||||
var arr = shop.get(family)
|
||||
return arr if arr is Array else []
|
||||
|
||||
|
||||
# ---- MetaTable 消费面(行集解析单点——MetaProgression/呈现层禁自扫表)----
|
||||
# 行族四轴(数据契约,行集归 meta.tres,数值全 provisional):
|
||||
# forge_products {attr, display_name, flat_value, base_price, price_ratio_pct} ·
|
||||
# boss_keystone {level_index, first_kill, repeat_kill} ·
|
||||
# workshop_items {item_id, display_name, clear_threshold} ·
|
||||
# conversion {attr, per_level_shift_pct}。
|
||||
|
||||
func meta_boss_keystone_row(level_index: int) -> Dictionary:
|
||||
for row in _meta_rows("boss_keystone"):
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 秘宝解锁混合制挂 treasure_items 行内(无 meta_workshop_items 访问器);
|
||||
# 秘宝解锁投影面=MetaProgression.treasure_shop_status
|
||||
|
||||
|
||||
func meta_conversion_row(attr: String) -> Dictionary:
|
||||
for row in _meta_rows("conversion"):
|
||||
if String(row.get("attr", "")) == attr:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# Y 刷新升级行单点读取(键 = 等级,缺行返回空字典=未购/已满级合法态)
|
||||
func meta_attune_refresh_row(level: int) -> Dictionary:
|
||||
for row in _meta_rows("attune_refresh_upgrade"):
|
||||
if int(row.get("level", -1)) == level:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态)
|
||||
func _meta_rows(family: String) -> Array:
|
||||
if meta == null:
|
||||
return []
|
||||
var arr = meta.get(family)
|
||||
return arr if arr is Array else []
|
||||
|
||||
|
||||
# ---- 锻造工坊(ForgeTable 消费面:两根类型轴默认值+六路线行族;禁自扫行集)----
|
||||
# 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:默认值归本表常驻持有(hero.tres 两轴已删——
|
||||
# 契约 2);路线行族=攻防数值成长唯一数据面(契约 1)。数值全 provisional 归 M4。
|
||||
|
||||
# 两根类型轴默认值(表未已入库=裸体态兜底 normal/light——与 ForgeTable 缺省值同口径)
|
||||
func forge_default_attack_type() -> String:
|
||||
return String(forge.default_attack_type) if forge != null else "normal"
|
||||
|
||||
|
||||
func forge_default_armor_type() -> String:
|
||||
return String(forge.default_armor_type) if forge != null else "light"
|
||||
|
||||
|
||||
# 六路线行族(副本——调用方禁改)
|
||||
func forge_routes() -> Array:
|
||||
return forge.routes.duplicate() if forge != null else []
|
||||
|
||||
|
||||
# 路线行单点(键 route_id;缺行=空字典)
|
||||
func forge_route(route_id: String) -> Dictionary:
|
||||
for row in forge_routes():
|
||||
if String(row.get("route_id", "")) == route_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 按类型轴反查武器路线(attack_type → 路线行;甲路线 id=甲型同值的显式化入口,避免消费侧依赖该巧合)
|
||||
func forge_weapon_route_by_type(attack_type: String) -> Dictionary:
|
||||
for row in forge_routes():
|
||||
if String(row.get("kind", "")) == "weapon" and String(row.get("type_axis", "")) == attack_type:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
|
||||
func forge_armor_route_by_type(armor_type: String) -> Dictionary:
|
||||
for row in forge_routes():
|
||||
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
|
||||
func skill_row(skill_id: String) -> Dictionary:
|
||||
return skill.row_by_id(skill_id)
|
||||
|
||||
|
||||
# ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
|
||||
|
||||
func set_reload_gate(gated: bool) -> void:
|
||||
|
||||
@@ -11,21 +11,43 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var st_tab: SettlementTable = loaded.get("settlement")
|
||||
if st == null or rt == null or st_tab == null:
|
||||
return # 单表校验已报错,不连锁
|
||||
# ①c F2 投放维度覆盖校验不启用:挖格不掉道具,秘宝
|
||||
# 唯一来路=三选一共享池〔节点/关卡完成〕;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
|
||||
# 静态校验。
|
||||
var effect_ids := _effect_ids_of(st)
|
||||
_cv_reward_dangling(rt, effect_ids, push_diag) # ① 增益词表悬空
|
||||
_cv_reward_treasure(loaded, rt, push_diag) # ①b aux 投放行双表
|
||||
_cv_poolmap(st_tab, effect_ids, push_diag) # ② 池映射悬空
|
||||
_cv_source_labels(st_tab, push_diag) # ③ 来源标签键集
|
||||
_cv_skill_pool(loaded, push_diag) # ⑤ 技能池引用完整性
|
||||
_cv_shelf_pool(loaded, push_diag) # ⑥ 商店货架 × 冒险共享池
|
||||
_cv_counter_closure(loaded, push_diag) # ⑦ 计数器引用闭合
|
||||
|
||||
|
||||
## 增益状态词表全集(effect_id → true)
|
||||
func _effect_ids_of(st: StatusEffectTable) -> Dictionary:
|
||||
var effect_ids := {}
|
||||
for row in st.rows:
|
||||
effect_ids[row.get("effect_id", "")] = true
|
||||
# ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
|
||||
# 例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
|
||||
# (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
|
||||
return effect_ids
|
||||
|
||||
|
||||
## ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
|
||||
## 例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
|
||||
## (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
|
||||
func _cv_reward_dangling(rt: RewardTable, effect_ids: Dictionary, push_diag: Callable) -> void:
|
||||
for row in rt.rows:
|
||||
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
|
||||
continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_treasure),不入增益词表面
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff):
|
||||
push_diag.call("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
|
||||
# ①b RewardTable aux 投放行双表校验(reward_dangling_treasure):aux 行 effect_id = item_id,
|
||||
# 须命中 TreasureItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
|
||||
# 后仍投放(连带错误),同拦。
|
||||
|
||||
|
||||
## ①b RewardTable aux 投放行双表校验(reward_dangling_treasure):aux 行 effect_id = item_id,
|
||||
## 须命中 TreasureItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
|
||||
## 后仍投放(连带错误),同拦。
|
||||
func _cv_reward_treasure(loaded: Dictionary, rt: RewardTable, push_diag: Callable) -> void:
|
||||
var treasure_live := {}
|
||||
var treasure_tab = loaded.get("treasure_items")
|
||||
if treasure_tab != null:
|
||||
@@ -39,27 +61,36 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
continue
|
||||
if not treasure_live.has(treasure_eff):
|
||||
push_diag.call("error", "reward_dangling_treasure", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), treasure_eff, "双表校验"])
|
||||
# ①c F2 投放维度覆盖校验不启用:挖格不掉道具,秘宝
|
||||
# 唯一来路=三选一共享池〔节点/关卡完成〕;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
|
||||
# 静态校验。
|
||||
# ② SettlementTable.effect_pool_map 悬空
|
||||
|
||||
|
||||
## ② SettlementTable.effect_pool_map 悬空
|
||||
func _cv_poolmap(st_tab: SettlementTable, effect_ids: Dictionary, push_diag: Callable) -> void:
|
||||
for eff in st_tab.core.effect_pool_map.keys():
|
||||
if not effect_ids.has(eff):
|
||||
push_diag.call("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
|
||||
# ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, seal_miss})
|
||||
|
||||
|
||||
## ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, seal_miss})
|
||||
func _cv_source_labels(st_tab: SettlementTable, push_diag: Callable) -> void:
|
||||
var required_sources := ["safe_cell", "seal_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "seal_miss"]
|
||||
for s in required_sources:
|
||||
if not st_tab.core.source_label_map.has(s):
|
||||
push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
|
||||
|
||||
## ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
func _cv_skill_pool(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var en: EnemyTable = loaded.get("enemy")
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
|
||||
push_diag.call("error", e.category, e.message)
|
||||
# ⑥ 商店货架 × 冒险共享池跨表校验(M-4——次数上限域):
|
||||
# pool_shelf kind 须命中 adventure.CURIO_KINDS 且为有限库存种(cap ∈ [1,99]——gp 无上限种/牌库外种
|
||||
# 入货架 = 库存账本无锚,加载致命;gp 袋负交易切边/规则改写型特性三轴切边由 ShopTable 词表封闭另拦)
|
||||
|
||||
|
||||
## ⑥ 商店货架 × 冒险共享池跨表校验(M-4——次数上限域):
|
||||
## pool_shelf kind 须命中 adventure.CURIO_KINDS 且为有限库存种(cap ∈ [1,99]——gp 无上限种/牌库外种
|
||||
## 入货架 = 库存账本无锚,加载致命;gp 袋负交易切边/规则改写型特性三轴切边由 ShopTable 词表封闭另拦)
|
||||
func _cv_shelf_pool(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var adv = loaded.get("adventure")
|
||||
var shp = loaded.get("shop")
|
||||
if adv != null and shp != null:
|
||||
@@ -78,34 +109,42 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
if quota < 1:
|
||||
push_diag.call("error", "shelf_zero_quota", "ShopTable.pool_shelf kind %s 令牌配额为 0(牌库空种入货架 = 库存无锚)" % kind)
|
||||
|
||||
# ⑦ 计数器引用闭合(卡② V9——读点 id 必命中同名写入行,否则加载致命):
|
||||
# 声明集 = feats + treasure 各行 triggers 内 kind=counter_add 的 do.counter_id;
|
||||
# 读点 = when fact=counter 的 id、do.value_counter、kind=counter_spend 的 counter_id。
|
||||
var feats_tab = loaded.get("feats")
|
||||
|
||||
## ⑦ 计数器引用闭合(卡② V9——读点 id 必命中同名写入行,否则加载致命):
|
||||
## 声明集 = feats + treasure 各行 triggers 内 kind=counter_add 的 do.counter_id;
|
||||
## 读点 = when fact=counter 的 id、do.value_counter、kind=counter_spend 的 counter_id。
|
||||
func _cv_counter_closure(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var trig_rows: Array[Variant] = []
|
||||
var feats_tab = loaded.get("feats")
|
||||
if feats_tab != null:
|
||||
for r in feats_tab.rows:
|
||||
trig_rows.append(r)
|
||||
var treasure_tab = loaded.get("treasure_items")
|
||||
if treasure_tab != null:
|
||||
for r in treasure_tab.rows:
|
||||
trig_rows.append(r)
|
||||
var counter_decls := _counter_decls_of(trig_rows)
|
||||
_cv_counter_reads(trig_rows, counter_decls, push_diag)
|
||||
|
||||
|
||||
## 计数器声明集收集:任一 do.counter_id 行(counter_add 直写、forgive_seal_miss 经 _apply_counter_hit 写——皆写入行)
|
||||
func _counter_decls_of(trig_rows: Array) -> Dictionary:
|
||||
var counter_decls := {}
|
||||
for row in trig_rows:
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
var dd: Dictionary[Variant, Variant] = (tr_v as Dictionary).get("do", {}) as Dictionary[Variant, Variant]
|
||||
# 声明集=任一 do.counter_id 行(counter_add 直写、forgive_seal_miss 经 _apply_counter_hit 写——皆写入行)
|
||||
if dd.has("counter_id"):
|
||||
counter_decls[String(dd.counter_id)] = true
|
||||
return counter_decls
|
||||
|
||||
|
||||
## 计数器读点校验:when counter id/do.value_counter/counter_spend 的 counter_id 必命中声明集
|
||||
func _cv_counter_reads(trig_rows: Array, counter_decls: Dictionary, push_diag: Callable) -> void:
|
||||
for row in trig_rows:
|
||||
var rid := String(row.get("feat_id", row.get("item_id", "")))
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
var tr: Dictionary[Variant, Variant] = tr_v as Dictionary[Variant, Variant]
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary[Variant, Variant]
|
||||
if String(c.get("fact", "")) == "counter" and c.has("id"):
|
||||
var rd := String(c.id)
|
||||
if not counter_decls.has(rd):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s when counter 读点无同名写入行: %s(引用闭合)" % [rid, rd])
|
||||
_check_counter_when(rid, tr, counter_decls, push_diag)
|
||||
var do2: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary[Variant, Variant]
|
||||
if do2.has("value_counter") and not counter_decls.has(String(do2.value_counter)):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s value_counter 读点无同名写入行: %s" % [rid, String(do2.value_counter)])
|
||||
@@ -113,6 +152,16 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
push_diag.call("error", "counter_dangling_ref", "%s counter_spend 无同名写入行: %s" % [rid, String(do2.counter_id)])
|
||||
|
||||
|
||||
## when counter 读点校验(fact=counter 条件的 id 必命中声明集)
|
||||
func _check_counter_when(rid: String, tr: Dictionary, counter_decls: Dictionary, push_diag: Callable) -> void:
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary[Variant, Variant]
|
||||
if String(c.get("fact", "")) == "counter" and c.has("id"):
|
||||
var rd := String(c.id)
|
||||
if not counter_decls.has(rd):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s when counter 读点无同名写入行: %s(引用闭合)" % [rid, rd])
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
|
||||
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
|
||||
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
|
||||
@@ -141,33 +190,25 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
|
||||
errs.append_array(_pool_cast_side_errs(skill_table, status_table)) # 施放类 side 守卫(buff→敌方侧/debuff→主角侧,防反向挂接——全表恰一次)
|
||||
for row in enemy_table.rows:
|
||||
var eid: String = String(row.get("enemy_id", ""))
|
||||
var basic_n := 0
|
||||
var buff_n := 0
|
||||
var debuff_n := 0
|
||||
var uncond_special_n := 0
|
||||
var pool_n := {"basic": 0, "buff": 0, "debuff": 0, "uncond_special": 0}
|
||||
for entry in row.get("pool", []):
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
|
||||
continue
|
||||
match skill_types[sid]:
|
||||
"basic":
|
||||
basic_n += 1
|
||||
"buff":
|
||||
buff_n += 1
|
||||
"debuff":
|
||||
debuff_n += 1
|
||||
if pool_n.has(skill_types[sid]):
|
||||
pool_n[skill_types[sid]] += 1
|
||||
# 无条件特殊技计数(普攻不计;trigger 缺省行计数——触发条件批 2026-10-26 池构成纪律)
|
||||
if skill_types[sid] != "basic" and not (skill_table.row_by_id(sid) if skill_table.has_method("row_by_id") else {}).is_empty():
|
||||
if skill_types[sid] != "basic":
|
||||
var srow_c: Dictionary[Variant, Variant] = skill_table.row_by_id(sid) if skill_table.has_method("row_by_id") else {}
|
||||
if not srow_c.has("trigger"):
|
||||
uncond_special_n += 1
|
||||
if basic_n != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
|
||||
if buff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
|
||||
if debuff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]})
|
||||
if uncond_special_n < 1:
|
||||
if not srow_c.is_empty() and not srow_c.has("trigger"):
|
||||
pool_n["uncond_special"] += 1
|
||||
if pool_n["basic"] != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, pool_n["basic"]]})
|
||||
if pool_n["buff"] > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, pool_n["buff"]]})
|
||||
if pool_n["debuff"] > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, pool_n["debuff"]]})
|
||||
if pool_n["uncond_special"] < 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池无条件特殊技行数 = 0——每池至少一行无条件特殊技纪律(触发条件批 2026-10-26)" % [eid]})
|
||||
return errs
|
||||
@@ -43,11 +43,7 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
return []
|
||||
var rows: Array[Dictionary] = []
|
||||
rows.assign(_enemy_rows_get.call())
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
var level_keys: Array[int] = _level_keys_of(rows)
|
||||
if level_keys.is_empty():
|
||||
_diag.call("fatal", "structural", "enemy 行集为空——锚行校验应已拦截")
|
||||
return []
|
||||
@@ -58,12 +54,7 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
_warn_resolve("enemy", "resolve_level_clamp", "enemy 请求 (%d,%d) clamp 解析(超出最大关卡取最大值)→ 关卡 %d" % [L, D, l_star], l_star, D)
|
||||
elif l_star != L:
|
||||
_warn_resolve("enemy", "resolve_level_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ 关卡 %d" % [L, D, l_star], l_star, D)
|
||||
var band_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind:
|
||||
var bv := int(row.get("depth_band", -1))
|
||||
if not band_keys.has(bv):
|
||||
band_keys.append(bv)
|
||||
var band_keys: Array[int] = _band_keys_of(rows, l_star, row_kind)
|
||||
if band_keys.is_empty():
|
||||
return []
|
||||
band_keys.sort()
|
||||
@@ -73,6 +64,32 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
_warn_resolve("enemy", "resolve_depth_clamp", "enemy 请求 (%d,%d) clamp 解析(超出顶带取顶带)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star)
|
||||
elif d_star != D:
|
||||
_warn_resolve("enemy", "resolve_depth_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star)
|
||||
return _band_rows(rows, l_star, row_kind, d_star)
|
||||
|
||||
|
||||
## 行集关卡键收集(纯行扫描——告警发射序不变)
|
||||
func _level_keys_of(rows: Array) -> Array[int]:
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
return level_keys
|
||||
|
||||
|
||||
## 带键收集(纯行扫描——l_star × row_kind 的 depth_band 去重集)
|
||||
func _band_keys_of(rows: Array, l_star: int, row_kind: String) -> Array[int]:
|
||||
var band_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind:
|
||||
var bv := int(row.get("depth_band", -1))
|
||||
if not band_keys.has(bv):
|
||||
band_keys.append(bv)
|
||||
return band_keys
|
||||
|
||||
|
||||
## 带行收集(纯行扫描——命中带全部行、按表文件行序)
|
||||
func _band_rows(rows: Array, l_star: int, row_kind: String, d_star: int) -> Array[Dictionary]:
|
||||
var all_out: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind \
|
||||
@@ -108,11 +125,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant
|
||||
if L < 1 or D < 0:
|
||||
_diag.call("fatal", "request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
|
||||
return {}
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
var level_keys: Array[int] = _level_keys_of(rows)
|
||||
if level_keys.is_empty():
|
||||
_diag.call("fatal", "structural", "%s 行集为空——锚行校验应已拦截" % table_id)
|
||||
return {}
|
||||
@@ -121,40 +134,59 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant
|
||||
var l_clamped := l_star < 0
|
||||
if l_clamped:
|
||||
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(可取范围内向上取整,超出用最大值)
|
||||
var depth_keys: Array[int] = _depth_keys_of(rows, l_star)
|
||||
depth_keys.sort()
|
||||
var d_star := _ceil_key(depth_keys, D)
|
||||
var d_clamped := d_star < 0
|
||||
if d_clamped:
|
||||
d_star = int(depth_keys[-1])
|
||||
var missing := _missing_label(l_clamped, d_clamped, l_star != L, d_star != D)
|
||||
if missing != "":
|
||||
_emit_missing_warning(table_id, missing, [l_star, d_star], [L, D])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 层键收集(纯行扫描——l_star 行的 node_depth 去重集)
|
||||
func _depth_keys_of(rows: Array, l_star: int) -> Array[int]:
|
||||
var depth_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star:
|
||||
var dv := int(row.get("node_depth", 0))
|
||||
if not depth_keys.has(dv):
|
||||
depth_keys.append(dv)
|
||||
depth_keys.sort()
|
||||
var d_star := _ceil_key(depth_keys, D)
|
||||
var d_clamped := d_star < 0
|
||||
if d_clamped:
|
||||
d_star = int(depth_keys[-1])
|
||||
var missing := ""
|
||||
return depth_keys
|
||||
|
||||
|
||||
## 解析偏移形态标签(clamp/ceil × level/depth/both 六形态——空串 = 精确命中;
|
||||
## l_ceil/d_ceil = 解析值 ≠ 请求值)
|
||||
func _missing_label(l_clamped: bool, d_clamped: bool, l_ceil: bool, d_ceil: bool) -> String:
|
||||
var label := ""
|
||||
if l_clamped and d_clamped:
|
||||
missing = "both_clamp"
|
||||
label = "both_clamp"
|
||||
elif l_clamped:
|
||||
missing = "level_clamp"
|
||||
label = "level_clamp"
|
||||
elif d_clamped:
|
||||
missing = "depth_clamp"
|
||||
elif l_star != L and d_star != D:
|
||||
missing = "both_ceil"
|
||||
elif l_star != L:
|
||||
missing = "level_ceil"
|
||||
elif d_star != D:
|
||||
missing = "depth_ceil"
|
||||
if missing != "":
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
_diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
label = "depth_clamp"
|
||||
elif l_ceil and d_ceil:
|
||||
label = "both_ceil"
|
||||
elif l_ceil:
|
||||
label = "level_ceil"
|
||||
elif d_ceil:
|
||||
label = "depth_ceil"
|
||||
return label
|
||||
|
||||
|
||||
## 偏移告警发射(四元组去重注册表语义逐字——R1 既有去重;
|
||||
## req/resolved = [L, D]/[l_star, d_star] 进度坐标对)
|
||||
func _emit_missing_warning(table_id: String, missing: String, resolved: Array, req: Array) -> void:
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, resolved[0], resolved[1]]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
_diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, req[0], req[1], verb, resolved[0], resolved[1], missing])
|
||||
|
||||
|
||||
# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
## 战斗域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):战斗编排域七表类型化持有。
|
||||
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
|
||||
class_name ConfigDomainCombat
|
||||
extends Resource
|
||||
|
||||
var board_gen: BoardGenTable
|
||||
var variation: VariationTable
|
||||
var status_effects: StatusEffectTable
|
||||
var enemy: EnemyTable
|
||||
var skill: SkillTable
|
||||
var hero: HeroTable
|
||||
var settlement: SettlementTable
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpy0a6fcggbwe
|
||||
@@ -0,0 +1,11 @@
|
||||
## 跨局域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):跨局元进度域五表类型化持有
|
||||
## (跨局循环=单局之外的长线进度域)。零方法零行为——纯持有结构;
|
||||
## 「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
|
||||
class_name ConfigDomainCrossrun
|
||||
extends Resource
|
||||
|
||||
var meta: MetaTable
|
||||
var meta_tree: MetaTreeTable
|
||||
var treasure_items: TreasureItemTable
|
||||
var feats: FeatTable
|
||||
var forge: ForgeTable
|
||||
@@ -0,0 +1 @@
|
||||
uid://du0gxv0lnrxnp
|
||||
@@ -0,0 +1,12 @@
|
||||
## 冒险域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):冒险 runner 域七表类型化持有。
|
||||
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
|
||||
class_name ConfigDomainRun
|
||||
extends Resource
|
||||
|
||||
var level: LevelTable
|
||||
var difficulty: DifficultyTable
|
||||
var level_gen_params: LevelGenParams
|
||||
var reward: RewardTable
|
||||
var adventure: AdventureTable
|
||||
var action_economy: ActionEconomyTable
|
||||
var shop: ShopTable
|
||||
@@ -0,0 +1 @@
|
||||
uid://jaxlqd8ml3co
|
||||
+17
-17
@@ -29,13 +29,13 @@ func reset() -> void:
|
||||
# relic 令牌档 = 行 rarity 直映射,词表外 transitional 值回退映射 rarest→rare/其余→excellent)
|
||||
func build() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
for kind in db.adventure.CURIO_KINDS:
|
||||
for tier in db.adventure.TIERS:
|
||||
for i in int(db.adventure.curio_quota(String(kind), String(tier))):
|
||||
for kind in db.run.adventure.CURIO_KINDS:
|
||||
for tier in db.run.adventure.TIERS:
|
||||
for i in int(db.run.adventure.curio_quota(String(kind), String(tier))):
|
||||
out.append({"kind": String(kind), "tier": String(tier)})
|
||||
for row in db.feat_rows():
|
||||
for row in db.crossrun.feats.rows:
|
||||
var rraw := String(row.get("rarity", ""))
|
||||
var rtier := rraw if db.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
|
||||
var rtier := rraw if db.run.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
|
||||
out.append({"kind": "relic", "tier": rtier, "feat_id": String(row.get("feat_id", ""))})
|
||||
return out
|
||||
|
||||
@@ -73,7 +73,7 @@ func pool_stock(kind: String) -> int:
|
||||
# 读数面 = 商店买走 vs 奖励领走 vs 枯竭)
|
||||
func stock_snapshot() -> Dictionary:
|
||||
var out := {}
|
||||
for row in db.shop_pool_rows():
|
||||
for row in db.run.shop.pool_rows():
|
||||
var kind := String(row.get("kind_id", ""))
|
||||
out[kind] = pool_stock(kind)
|
||||
return out
|
||||
@@ -97,23 +97,23 @@ func consume_deck_token(kind: String, tier: String, feat_id: String = "") -> boo
|
||||
func cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
var cap := 99
|
||||
if kind == "relic":
|
||||
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id)
|
||||
cap = int(row.get("cap", 1)) if not row.is_empty() else 1
|
||||
return taken.get("relic:" + feat_id, 0) < cap
|
||||
if not db.adventure.CURIO_KINDS.has(kind):
|
||||
if not db.run.adventure.CURIO_KINDS.has(kind):
|
||||
return true
|
||||
cap = int(db.adventure.curio_cap(kind))
|
||||
cap = int(db.run.adventure.curio_cap(kind))
|
||||
return int(taken.get(kind, 0)) < cap
|
||||
|
||||
|
||||
# 专长凭证展示判定(四口径:一局限购一次/稀有只在概率不在价格/计步保底/触发清零。
|
||||
# 调用即一次展示判定与记账——展示次数计 pity、配额与限购双闸)
|
||||
func feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
|
||||
if ticket_sold >= int(db.adventure.feat_ticket_limit):
|
||||
if ticket_sold >= int(db.run.adventure.feat_ticket_limit):
|
||||
return false # 限购满——不再触发(保底冻结)
|
||||
if ticket_appeared >= int(db.adventure.feat_ticket_quota):
|
||||
if ticket_appeared >= int(db.run.adventure.feat_ticket_quota):
|
||||
return false # 配额满——本局不再出现
|
||||
var offer: bool = ticket_pity >= int(db.adventure.feat_ticket_pity_step) or rg.randi_range(1, 100) <= int(db.adventure.feat_ticket_chance)
|
||||
var offer: bool = ticket_pity >= int(db.run.adventure.feat_ticket_pity_step) or rg.randi_range(1, 100) <= int(db.run.adventure.feat_ticket_chance)
|
||||
if offer:
|
||||
ticket_pity = 0
|
||||
ticket_appeared += 1
|
||||
@@ -160,7 +160,7 @@ func _candidate_pool(channel: String) -> Array[Variant]:
|
||||
func _draw_weighted(pool: Array[Variant], rg: RandomNumberGenerator) -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var seen: Array[Variant] = []
|
||||
var want := int(db.adventure.curio_candidate_count)
|
||||
var want := int(db.run.adventure.curio_candidate_count)
|
||||
while out.size() < want and not pool.is_empty():
|
||||
var total := _pool_weight(pool)
|
||||
if total <= 0:
|
||||
@@ -180,13 +180,13 @@ func _draw_weighted(pool: Array[Variant], rg: RandomNumberGenerator) -> Array:
|
||||
func _pool_weight(pool: Array[Variant]) -> int:
|
||||
var total := 0
|
||||
for tok in pool:
|
||||
total += db.adventure.boss_feat_weight(String((tok as Dictionary).get("tier", "")))
|
||||
total += db.run.adventure.boss_feat_weight(String((tok as Dictionary).get("tier", "")))
|
||||
return total
|
||||
|
||||
|
||||
func _weighted_index(pool: Array[Variant], roll: int) -> int:
|
||||
for i in pool.size():
|
||||
roll -= db.adventure.boss_feat_weight(String((pool[i] as Dictionary).get("tier", "")))
|
||||
roll -= db.run.adventure.boss_feat_weight(String((pool[i] as Dictionary).get("tier", "")))
|
||||
if roll <= 0:
|
||||
return i
|
||||
return 0
|
||||
@@ -198,12 +198,12 @@ func _draw_exposure(pool: Array[Variant], rg: RandomNumberGenerator) -> Array:
|
||||
_shuffle(pool, rg)
|
||||
var sub: Array[Variant] = []
|
||||
for tok in pool:
|
||||
if db.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT):
|
||||
if db.run.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.run.adventure.tier_rank(db.run.adventure.TIER_EXCELLENT):
|
||||
sub.append(tok)
|
||||
_shuffle(sub, rg)
|
||||
var out: Array[Variant] = []
|
||||
var seen: Array[Variant] = []
|
||||
var want := int(db.adventure.curio_candidate_count)
|
||||
var want := int(db.run.adventure.curio_candidate_count)
|
||||
var n_full := maxi(1, want - 1) if want >= 2 else want
|
||||
while out.size() < want:
|
||||
var src: Array[Variant] = sub if out.size() >= n_full and not sub.is_empty() else pool
|
||||
|
||||
@@ -56,7 +56,7 @@ func rebind_level_runner(runner: LevelRunner, level_index: int) -> void:
|
||||
func open_level_clear(payload_base: Dictionary) -> void:
|
||||
var hp_before := _stats.hero_hp
|
||||
var max_eff := _stats.effective_max_hp() # 回血封顶口径含调试浮窗 pct(同战斗内上限)
|
||||
var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
|
||||
var heal := mini(int(floor(float(max_eff) * float(int(db.run.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
|
||||
_stats.hero_hp = hp_before + heal
|
||||
_bank.reset_x() # 碰三选一重置 X 余量(不结转)
|
||||
_bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
@@ -109,7 +109,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||||
return false
|
||||
if kind == "feat_ticket":
|
||||
return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集)
|
||||
var value: int = int(db.adventure.curio_value(kind, tier))
|
||||
var value: int = int(db.run.adventure.curio_value(kind, tier))
|
||||
var hp_before := _stats.hero_hp
|
||||
if not _apply_curio_pick(kind, tier, value, feat_id):
|
||||
push_error("[停点件] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
|
||||
@@ -122,7 +122,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||||
func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||||
if feat_id == "":
|
||||
return {}
|
||||
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id)
|
||||
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||||
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从牌库摘除该令牌(选中踢池;
|
||||
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。
|
||||
func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = "") -> bool:
|
||||
if kind == "relic" and db.feat_row(feat_id).is_empty():
|
||||
if kind == "relic" and db.crossrun.feats.row_by_id(feat_id).is_empty():
|
||||
push_error("[停点件] relic 兑付失败——表无此件: %s" % feat_id)
|
||||
return false # 先校验后摘牌(令牌零丢失)
|
||||
if not _deck.consume_deck_token(kind, tier, feat_id):
|
||||
@@ -172,7 +172,7 @@ func _apply_resource_pick(kind: String, value: int, feat_id: String) -> bool:
|
||||
# 路由加算段(match 收敛进效果类型:属性值提升四类走 adventure_table.CURIO_APPLY_ROUTES 声明
|
||||
# 路由(bonus 落点键),加新加算类令牌零代码)
|
||||
func _apply_route_pick(kind: String, value: int) -> bool:
|
||||
var sink := db.adventure.curio_apply_route(kind)
|
||||
var sink := db.run.adventure.curio_apply_route(kind)
|
||||
if sink == "":
|
||||
return false
|
||||
_journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍)
|
||||
@@ -229,7 +229,7 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") ->
|
||||
if _deck.find_token(candidates, kind, tier, feat_id).is_empty():
|
||||
push_error("[停点件] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
|
||||
return false
|
||||
var value: int = int(db.adventure.curio_value(kind, tier))
|
||||
var value: int = int(db.run.adventure.curio_value(kind, tier))
|
||||
if not _apply_curio_pick(kind, tier, value, feat_id):
|
||||
push_error("[停点件] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
|
||||
return false
|
||||
@@ -348,10 +348,10 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id:
|
||||
func consume_pool_purchase(kind: String, tier: String) -> bool:
|
||||
if kind == "feat_ticket":
|
||||
return _redeem_feat_ticket()
|
||||
if not db.adventure.CURIO_KINDS.has(kind):
|
||||
if not db.run.adventure.CURIO_KINDS.has(kind):
|
||||
push_error("[停点件] 商店购买消费拒绝——件不在池词表: %s" % kind)
|
||||
return false
|
||||
return _apply_curio_pick(kind, tier, int(db.adventure.curio_value(kind, tier)))
|
||||
return _apply_curio_pick(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
|
||||
|
||||
|
||||
# ---- 冒险级复位(宿主 _reset_curio_state 调用——停点半:池归 deck.reset、停点三态与刷新账清零)----
|
||||
@@ -376,7 +376,7 @@ func restore_pending(bp_pending: Dictionary) -> void:
|
||||
mig["feat_id"] = String((c as Dictionary).get("feat_id", ""))
|
||||
migrated.append(mig)
|
||||
else:
|
||||
migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON})
|
||||
migrated.append({"kind": String(c), "tier": db.run.adventure.TIER_COMMON})
|
||||
_pending = {"candidates": migrated}
|
||||
var payload: Dictionary[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
curio_choice_presented.emit(payload)
|
||||
|
||||
@@ -294,7 +294,7 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
|
||||
func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary:
|
||||
var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array)
|
||||
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
|
||||
var threshold: int = defender_ac(target) + int(db.settlement.damage_roll.attack_bonus)
|
||||
var threshold: int = defender_ac(target) + int(db.combat.settlement.damage_roll.attack_bonus)
|
||||
# armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面)
|
||||
var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
|
||||
var trade_hit := 0
|
||||
@@ -383,7 +383,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
var ac_hits: Array[Variant] = ctx["ac_hits"]
|
||||
var roll_read: Dictionary[Variant, Variant] = ctx["roll_read"]
|
||||
var atk_type: String = ctx["atk_type"]
|
||||
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.damage_roll.crit_multiplier), int(ctx["cdelta"]))
|
||||
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.combat.settlement.damage_roll.crit_multiplier), int(ctx["cdelta"]))
|
||||
var payload: Dictionary[Variant, Variant] = {"target": String(target.display_name), "target_id": String(target.get("enemy_id", ""))}
|
||||
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
|
||||
payload["roll"] = int(roll_read.roll)
|
||||
@@ -392,7 +392,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
if bool(ctx["crit"]):
|
||||
payload["crit"] = true
|
||||
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense"))
|
||||
var landed: int = maxi(int(db.settlement.damage_roll.damage_floor), pooled)
|
||||
var landed: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled)
|
||||
# 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区
|
||||
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
|
||||
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
|
||||
@@ -406,7 +406,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
hero_rider += contrib
|
||||
if hero_rider > 0:
|
||||
if bool(ctx["crit"]) and bool(inject["rider_tier_crit"]):
|
||||
hero_rider = int(round(float(hero_rider) * float(db.settlement.damage_roll.crit_multiplier)))
|
||||
hero_rider = int(round(float(hero_rider) * float(db.combat.settlement.damage_roll.crit_multiplier)))
|
||||
landed += hero_rider
|
||||
payload["hero_rider"] = hero_rider
|
||||
units.apply_damage(target, landed)
|
||||
@@ -432,7 +432,7 @@ func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void:
|
||||
# source_class 用中性名(fx_overflow 泛用通道,不挂件名)
|
||||
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow", "贯穿弹射",
|
||||
{"channel": "board",
|
||||
"base_value": maxi(int(db.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
|
||||
"base_value": maxi(int(db.combat.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
|
||||
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": int(t.round_index),
|
||||
"origin_cell": t.origin_cell, "attack_type": "pierce"})
|
||||
if policy == POLICY_SEGMENT:
|
||||
@@ -492,13 +492,13 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
func _enemy_roll(t: Dictionary) -> Dictionary:
|
||||
var hero: Dictionary[Variant, Variant] = units.hero_unit()
|
||||
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
|
||||
var threshold: int = defender_ac(hero) + int(db.settlement.damage_roll.attack_bonus)
|
||||
var threshold: int = defender_ac(hero) + int(db.combat.settlement.damage_roll.attack_bonus)
|
||||
var crit: bool = bool(roll_read.crit)
|
||||
var discount: int = _roll_discount(int(roll_read.roll), threshold)
|
||||
var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light")))
|
||||
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.damage_roll.crit_multiplier), cdelta)
|
||||
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.combat.settlement.damage_roll.crit_multiplier), cdelta)
|
||||
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output"))
|
||||
var total: int = maxi(int(db.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
|
||||
var total: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
|
||||
return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total}
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ func _enemy_shield_apply(hits: Array, total: int) -> Dictionary:
|
||||
var set_v = h.get("set", null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
if shield > 0:
|
||||
total = maxi(int(db.settlement.damage_roll.damage_floor), total - shield)
|
||||
total = maxi(int(db.combat.settlement.damage_roll.damage_floor), total - shield)
|
||||
return {"total": total, "shield": shield}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ func _resolve_board_typed(t: Dictionary) -> void:
|
||||
# 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面)
|
||||
if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "":
|
||||
cdelta += int(h.delta)
|
||||
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_roll.damage_floor))
|
||||
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.combat.settlement.damage_roll.damage_floor))
|
||||
units.apply_damage(target, dealt)
|
||||
emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index),
|
||||
"%s → %s" % [String(t.display), String(target.display_name)], dealt,
|
||||
@@ -556,7 +556,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
|
||||
_ineffective_row(t, "无存活敌人")
|
||||
_outcomes[int(t.ticket_id)] = "zero"
|
||||
return
|
||||
var blast_dmg: int = maxi(int(db.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||||
var blast_dmg: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||||
for u in alive:
|
||||
units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手
|
||||
emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index),
|
||||
@@ -567,7 +567,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
|
||||
|
||||
# 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷
|
||||
func _resolve_board_flat_hero(t: Dictionary) -> void:
|
||||
var dealt: int = maxi(int(db.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||||
var dealt: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||||
units.apply_damage(units.hero_unit(), dealt)
|
||||
var extra: Dictionary[Variant, Variant] = {"target": "hero"}
|
||||
for k in t.context.keys():
|
||||
@@ -580,16 +580,16 @@ func _resolve_board_flat_hero(t: Dictionary) -> void:
|
||||
|
||||
# 护甲等级掷点(armor_roll 封闭流——逐次掷逐次取号、逐位可回放):返回 {roll, crit}
|
||||
func _armor_roll() -> Dictionary:
|
||||
var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.damage_roll.armor_roll_stream))
|
||||
var roll: int = stream.randi_range(1, int(db.settlement.damage_roll.roll_faces))
|
||||
return {"roll": roll, "crit": roll == int(db.settlement.damage_roll.crit_roll)}
|
||||
var stream: RandomNumberGenerator = rng.open_stream(String(db.combat.settlement.damage_roll.armor_roll_stream))
|
||||
var roll: int = stream.randi_range(1, int(db.combat.settlement.damage_roll.roll_faces))
|
||||
return {"roll": roll, "crit": roll == int(db.combat.settlement.damage_roll.crit_roll)}
|
||||
|
||||
|
||||
# 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点)
|
||||
func _roll_discount(roll: int, threshold: int) -> int:
|
||||
if roll == int(db.settlement.damage_roll.crit_roll):
|
||||
if roll == int(db.combat.settlement.damage_roll.crit_roll):
|
||||
return 100
|
||||
return DamageMath.roll_discount_percent(roll, threshold, db.settlement.damage_roll.roll_tier_bounds, db.settlement.damage_roll.roll_tier_discounts)
|
||||
return DamageMath.roll_discount_percent(roll, threshold, db.combat.settlement.damage_roll.roll_tier_bounds, db.combat.settlement.damage_roll.roll_tier_discounts)
|
||||
|
||||
|
||||
# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照;
|
||||
@@ -599,15 +599,15 @@ func defender_ac(unit: Dictionary) -> int:
|
||||
if bool(unit.get("is_hero", false)):
|
||||
var total: int = _hero_ac_override
|
||||
if total < 0:
|
||||
total = int(db.hero.armor_class)
|
||||
total = int(db.combat.hero.armor_class)
|
||||
for eff in effects.query_active("hero"):
|
||||
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
|
||||
if not row.is_empty() and String(row.unit) == "ac":
|
||||
total += int(eff.effective_value)
|
||||
return total
|
||||
var total_e: int = int(unit.get("armor_class", 0))
|
||||
for eff_e in effects.query_active("enemy"):
|
||||
var erow_ac: Dictionary[Variant, Variant] = db.status_effect_row(eff_e.effect_id)
|
||||
var erow_ac: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff_e.effect_id)
|
||||
if not erow_ac.is_empty() and String(erow_ac.unit) == "ac":
|
||||
total_e += int(eff_e.effective_value)
|
||||
return total_e
|
||||
@@ -618,14 +618,14 @@ func defender_ac(unit: Dictionary) -> int:
|
||||
func _counter_delta(attack_type: String, armor: String) -> int:
|
||||
if attack_type == "":
|
||||
return 0
|
||||
var tbl: Dictionary[Variant, Variant] = db.settlement.damage_roll.armor_effectiveness
|
||||
var tbl: Dictionary[Variant, Variant] = db.combat.settlement.damage_roll.armor_effectiveness
|
||||
var row: Dictionary[Variant, Variant] = tbl.get(attack_type, {})
|
||||
return int(row.get(armor, 0))
|
||||
|
||||
|
||||
# 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
|
||||
func _discount_shift_up(discount: int, shift: int) -> int:
|
||||
var tbl: Array[Variant] = db.settlement.damage_roll.roll_tier_discounts
|
||||
var tbl: Array[Variant] = db.combat.settlement.damage_roll.roll_tier_discounts
|
||||
var idx := tbl.find(discount)
|
||||
if idx < 0:
|
||||
return discount
|
||||
@@ -636,12 +636,12 @@ func _discount_shift_up(discount: int, shift: int) -> int:
|
||||
func clamped_pool(pool: String) -> int:
|
||||
var total := 0
|
||||
for eff in effects.query_active("hero") + effects.query_active("enemy"):
|
||||
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
if String(db.settlement.core.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
|
||||
if String(db.combat.settlement.core.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
|
||||
total += eff.effective_value # 池内加算(L×m 原样——越域不 clamp 由本处收口)
|
||||
return clampi(total, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_hi))
|
||||
return clampi(total, int(db.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.pool_clamp_hi))
|
||||
|
||||
|
||||
# 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制")
|
||||
|
||||
+170
-110
@@ -94,58 +94,76 @@ func _init(db_ref: ConfigDb = null) -> void:
|
||||
## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随宿主字典引用跨战持久——无副本搬运)。
|
||||
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
|
||||
_triggers.clear()
|
||||
if db != null and db.feats != null:
|
||||
for row in db.feats.rows:
|
||||
_load_feat_triggers(feat_ids)
|
||||
_load_attune_triggers(item_ids)
|
||||
_push_tree_rules(tree_rules)
|
||||
|
||||
|
||||
## 特性订阅装载域(feats 表序→按持有集过滤→行内数组序——装载声明序第一段)
|
||||
func _load_feat_triggers(feat_ids: Array) -> void:
|
||||
if db != null and db.crossrun.feats != null:
|
||||
for row in db.crossrun.feats.rows:
|
||||
var fid := String(row.get("feat_id", ""))
|
||||
if not feat_ids.has(fid):
|
||||
continue
|
||||
for tr in (row.get("triggers", []) as Array):
|
||||
_push_trigger("feat", fid, tr, row)
|
||||
|
||||
|
||||
## 同调秘宝订阅装载域(treasure 同调行序→按装备集过滤→行内数组序——装载声明序第二段)
|
||||
func _load_attune_triggers(item_ids: Array) -> void:
|
||||
if db != null:
|
||||
for row in db.treasure_attunement_rows():
|
||||
for row in db.crossrun.treasure_items.attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if not item_ids.has(iid):
|
||||
continue
|
||||
for tr in (row.get("triggers", []) as Array):
|
||||
_push_trigger("attune", iid, tr, row)
|
||||
# 树规则合成订阅(六键按 v0.8 三组双态):posture 两键=「每关首轮窗」语义
|
||||
# 〔battle_index==1 且 round_index==1;battle_index=关卡域战斗序由 enc 携带〕;
|
||||
# sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同
|
||||
# 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕;
|
||||
# economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。
|
||||
|
||||
|
||||
## 树规则合成订阅域(六键按 v0.8 三组双态——装载声明序第三段):posture 两键=「每关首轮窗」语义
|
||||
## 〔battle_index==1 且 round_index==1;battle_index=关卡域战斗序由 enc 携带〕;
|
||||
## sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同
|
||||
## 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕;
|
||||
## economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。
|
||||
func _push_tree_rules(tree_rules: Array) -> void:
|
||||
for key_v in tree_rules:
|
||||
var rk := String(key_v)
|
||||
match rk:
|
||||
"posture_first_strike":
|
||||
_push_trigger("tree", rk, {"on": "round_start",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
|
||||
"posture_bulwark":
|
||||
_push_trigger("tree", rk, {"on": "damage_taken",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "shield", "value": 1}}, {})
|
||||
"sense_sharp":
|
||||
# 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct,
|
||||
# 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动)
|
||||
_push_trigger("tree", rk, {"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "damage_mult", "value": 50}}, {})
|
||||
_push_trigger("tree", rk, {"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used",
|
||||
"value": 1}}, {})
|
||||
"sense_guard":
|
||||
# 守=标错返还行动点·每关限次(契约 12 失误补偿——seal_missed 由 _judge_seal 去重闸后
|
||||
# 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立)
|
||||
_push_trigger("tree", rk, {"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
|
||||
_push_trigger("tree", rk, {"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used",
|
||||
"value": 1}}, {})
|
||||
_push_tree_rule(String(key_v))
|
||||
|
||||
|
||||
## 单树规则合成行 push(六键 match 分派——词表封闭,未知键零订阅零扰动)
|
||||
func _push_tree_rule(rk: String) -> void:
|
||||
match rk:
|
||||
"posture_first_strike":
|
||||
_push_trigger("tree", rk, {"on": "round_start",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
|
||||
"posture_bulwark":
|
||||
_push_trigger("tree", rk, {"on": "damage_taken",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "shield", "value": 1}}, {})
|
||||
"sense_sharp":
|
||||
# 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct,
|
||||
# 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动)
|
||||
_push_trigger("tree", rk, {"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "damage_mult", "value": 50}}, {})
|
||||
_push_trigger("tree", rk, {"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used",
|
||||
"value": 1}}, {})
|
||||
"sense_guard":
|
||||
# 守=标错返还行动点·每关限次(契约 12 失误补偿——seal_missed 由 _judge_seal 去重闸后
|
||||
# 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立)
|
||||
_push_trigger("tree", rk, {"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
|
||||
_push_trigger("tree", rk, {"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used",
|
||||
"value": 1}}, {})
|
||||
|
||||
|
||||
func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void:
|
||||
@@ -203,13 +221,14 @@ func dispatch(ctx: EffectContext, event: String, payload: Dictionary, facts: Cal
|
||||
return hits
|
||||
|
||||
|
||||
## 计数器归属桶解析(前缀规则唯一出处——round_/lvl_/adv_ 显式桶,其余落 battle 缺省桶)。
|
||||
## 计数器归属桶解析(前缀规则唯一出处——round_/lvl_/adv_ 显式桶,其余落 battle 缺省桶;
|
||||
## 前缀判定序 round→lvl→adv→battle 缺省逐字)
|
||||
static func bucket_of(ctx: EffectContext, counter_id: String) -> Dictionary[Variant, Variant]:
|
||||
if counter_id.begins_with("round_"):
|
||||
return ctx.round_bucket
|
||||
elif counter_id.begins_with("lvl_"):
|
||||
if counter_id.begins_with("lvl_"):
|
||||
return ctx.level_bucket
|
||||
elif counter_id.begins_with("adv_"):
|
||||
if counter_id.begins_with("adv_"):
|
||||
return ctx.adventure_bucket
|
||||
return ctx.battle_bucket
|
||||
|
||||
@@ -262,41 +281,68 @@ func _cond_ok(ctx: EffectContext, c: Dictionary, payload: Dictionary, facts: Cal
|
||||
var val = c.get("value", null)
|
||||
var actual = _fact_value(ctx, fact, c, payload, facts)
|
||||
match op:
|
||||
"eq":
|
||||
return str(actual) == str(val)
|
||||
"neq":
|
||||
return str(actual) != str(val)
|
||||
"gt":
|
||||
return int(actual) > int(val)
|
||||
"gte":
|
||||
return int(actual) >= int(val)
|
||||
"lt":
|
||||
return int(actual) < int(val)
|
||||
"lte":
|
||||
return int(actual) <= int(val)
|
||||
"eq", "neq", "gt", "gte", "lt", "lte":
|
||||
return _cmp_ok(op, actual, val)
|
||||
"ends_with":
|
||||
if val is Array:
|
||||
for v in (val as Array):
|
||||
if String(actual).ends_with(String(v)):
|
||||
return true
|
||||
return false
|
||||
return String(actual).ends_with(String(val))
|
||||
return _ends_with_ok(actual, val)
|
||||
"has":
|
||||
# 在场判定(fact 查询面落法——白名单覆盖):value 缺省=非空判定
|
||||
# (terrain_kind fact:伤害型变异集非空=挖开伤害型地形格/邻格有伤害型地形/地形伤种类在场);
|
||||
# value 给定=数组包含判定(保留泛用面)。
|
||||
if c.has("value"):
|
||||
if actual is Array:
|
||||
return (actual as Array).has(val)
|
||||
return false
|
||||
if actual is Array:
|
||||
return not (actual as Array).is_empty()
|
||||
return actual != null and String(actual) != "" # null 安全(敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错)
|
||||
return _has_ok(c, actual)
|
||||
"mod_zero", "mod_eq", "advantageous", "gte_ref", "lt_ref":
|
||||
return _counter_op_ok(ctx, c, op, actual)
|
||||
assert(false, "[效果引擎] when 算子出域: %s" % op)
|
||||
return false
|
||||
|
||||
|
||||
## 比较算子域(eq/neq/gt/gte/lt/lte 六比较——str 逐字与 int 投影语义逐字)
|
||||
func _cmp_ok(op: String, actual: Variant, val: Variant) -> bool:
|
||||
var out := false
|
||||
match op:
|
||||
"eq":
|
||||
out = str(actual) == str(val)
|
||||
"neq":
|
||||
out = str(actual) != str(val)
|
||||
"gt":
|
||||
out = int(actual) > int(val)
|
||||
"gte":
|
||||
out = int(actual) >= int(val)
|
||||
"lt":
|
||||
out = int(actual) < int(val)
|
||||
"lte":
|
||||
out = int(actual) <= int(val)
|
||||
return out
|
||||
|
||||
|
||||
## 后缀匹配域(ends_with——值数组任一命中即真)
|
||||
func _ends_with_ok(actual: Variant, val: Variant) -> bool:
|
||||
if val is Array:
|
||||
for v in (val as Array):
|
||||
if String(actual).ends_with(String(v)):
|
||||
return true
|
||||
return false
|
||||
return String(actual).ends_with(String(val))
|
||||
|
||||
|
||||
## 在场判定域(has——fact 查询面落法,白名单覆盖):value 缺省=非空判定
|
||||
## (terrain_kind fact:伤害型变异集非空=挖开伤害型地形格/邻格有伤害型地形/地形伤种类在场);
|
||||
## value 给定=数组包含判定(保留泛用面)
|
||||
func _has_ok(c: Dictionary, actual: Variant) -> bool:
|
||||
if c.has("value"):
|
||||
if actual is Array:
|
||||
return (actual as Array).has(c.get("value"))
|
||||
return false
|
||||
if actual is Array:
|
||||
return not (actual as Array).is_empty()
|
||||
return actual != null and String(actual) != "" # null 安全(敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错)
|
||||
|
||||
|
||||
## 计数器与矩阵算子域(mod_zero/mod_eq 取模、advantageous 克制矩阵、gte_ref/lt_ref 计数器比较——读经 ctx)
|
||||
func _counter_op_ok(ctx: EffectContext, c: Dictionary, op: String, actual: Variant) -> bool:
|
||||
match op:
|
||||
"mod_zero":
|
||||
var m := int(val)
|
||||
var m := int(c.get("value", null))
|
||||
return m > 0 and int(actual) % m == 0
|
||||
"mod_eq":
|
||||
var me_m := int(val)
|
||||
var me_m := int(c.get("value", null))
|
||||
return me_m > 0 and int(actual) % me_m == int(c.get("ref", 0))
|
||||
"advantageous":
|
||||
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
|
||||
@@ -305,7 +351,6 @@ func _cond_ok(ctx: EffectContext, c: Dictionary, payload: Dictionary, facts: Cal
|
||||
return int(actual) >= counter(ctx, String(c.get("ref", "")))
|
||||
"lt_ref":
|
||||
return int(actual) < counter(ctx, String(c.get("ref", "")))
|
||||
assert(false, "[效果引擎] when 算子出域: %s" % op)
|
||||
return false
|
||||
|
||||
|
||||
@@ -329,7 +374,7 @@ func _counter_delta_of(matchup: String) -> int:
|
||||
var parts := matchup.split("_vs_")
|
||||
if parts.size() != 2:
|
||||
return 0
|
||||
var tbl: Dictionary[Variant, Variant] = db.settlement.damage_roll.armor_effectiveness
|
||||
var tbl: Dictionary[Variant, Variant] = db.combat.settlement.damage_roll.armor_effectiveness
|
||||
var row: Dictionary[Variant, Variant] = tbl.get(String(parts[0]), {})
|
||||
return int(row.get(String(parts[1]), 0))
|
||||
|
||||
@@ -350,44 +395,59 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array,
|
||||
errs.append({"category": "enum", "message": "%s 订阅事件出域(事件词汇封闭): %s" % [rid, on]})
|
||||
elif MINE_EVENTS.has(on):
|
||||
mine_covered = true
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary
|
||||
for k in c.keys():
|
||||
if not WHEN_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s when 键出白名单: %s" % [rid, String(k)]})
|
||||
var fact := String(c.get("fact", ""))
|
||||
if not FACTS.has(fact):
|
||||
errs.append({"category": "enum", "message": "%s fact 出域(事实词汇封闭 %d 项): %s" % [rid, FACTS.size(), fact]})
|
||||
if not OPS.has(String(c.get("op", "eq"))):
|
||||
errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]})
|
||||
var do_d: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary
|
||||
for k in do_d.keys():
|
||||
if not DO_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s do 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(k)]})
|
||||
var kind := String(do_d.get("kind", ""))
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s 效果类型出域(解释器闭集): %s" % [rid, kind]})
|
||||
# 计数器前缀归属合法性(.tres 行路径——写错前缀加载致命;判据单点 counter_id_legal,与 _push_trigger 同源)
|
||||
if do_d.has("counter_id"):
|
||||
var cid := String(do_d.counter_id)
|
||||
if not counter_id_legal(cid):
|
||||
errs.append({"category": "enum", "message": "%s counter_id 前缀非法(meta_ 出域/引擎不接存档级): %s" % [rid, cid]})
|
||||
# value_from/delta_from/mult_from/cap_from 引用面:行字段存在性(悬空=加载致命)——
|
||||
# do 与 when 两侧同面(when value_from=dawn_order effective_rounds 先例)
|
||||
for vf_k in ["value_from", "delta_from", "mult_from", "cap_from"]:
|
||||
if do_d.has(vf_k):
|
||||
var fld := String(do_d.get(vf_k, ""))
|
||||
if fld != "" and not row.has(fld):
|
||||
errs.append({"category": "dangling", "message": "%s do.%s 悬空(行无字段 %s)" % [rid, vf_k, fld]})
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c_w: Dictionary[Variant, Variant] = c_v as Dictionary
|
||||
if c_w.has("value_from"):
|
||||
var wfld := String(c_w.get("value_from", ""))
|
||||
if wfld != "" and not row.has(wfld):
|
||||
errs.append({"category": "dangling", "message": "%s when.value_from 悬空(行无字段 %s)" % [rid, wfld]})
|
||||
_vtr_when_keys(rid, tr, errs)
|
||||
_vtr_do_keys(rid, tr, row, errs)
|
||||
return mine_covered
|
||||
|
||||
|
||||
## when 键域校验:键白名单/fact 词表/算子词表(追加序承原位——when 键→fact→op)
|
||||
static func _vtr_when_keys(rid: String, tr: Dictionary, errs: Array) -> void:
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary
|
||||
for k in c.keys():
|
||||
if not WHEN_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s when 键出白名单: %s" % [rid, String(k)]})
|
||||
var fact := String(c.get("fact", ""))
|
||||
if not FACTS.has(fact):
|
||||
errs.append({"category": "enum", "message": "%s fact 出域(事实词汇封闭 %d 项): %s" % [rid, FACTS.size(), fact]})
|
||||
if not OPS.has(String(c.get("op", "eq"))):
|
||||
errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]})
|
||||
|
||||
|
||||
## do 键域校验:键白名单(零裸 flat 护栏)/效果类型闭集/计数器前缀合法性+value_from 引用悬空域
|
||||
static func _vtr_do_keys(rid: String, tr: Dictionary, row: Dictionary, errs: Array) -> void:
|
||||
var do_d: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary
|
||||
for k in do_d.keys():
|
||||
if not DO_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s do 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(k)]})
|
||||
var kind := String(do_d.get("kind", ""))
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s 效果类型出域(解释器闭集): %s" % [rid, kind]})
|
||||
# 计数器前缀归属合法性(.tres 行路径——写错前缀加载致命;判据单点 counter_id_legal,与 _push_trigger 同源)
|
||||
if do_d.has("counter_id"):
|
||||
var cid := String(do_d.counter_id)
|
||||
if not counter_id_legal(cid):
|
||||
errs.append({"category": "enum", "message": "%s counter_id 前缀非法(meta_ 出域/引擎不接存档级): %s" % [rid, cid]})
|
||||
_vtr_from_refs(rid, tr, row, errs)
|
||||
|
||||
|
||||
## value_from/delta_from/mult_from/cap_from 引用面:行字段存在性(悬空=加载致命)——
|
||||
## do 与 when 两侧同面(when value_from=dawn_order effective_rounds 先例)
|
||||
static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: Array) -> void:
|
||||
var do_d: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary
|
||||
for vf_k in ["value_from", "delta_from", "mult_from", "cap_from"]:
|
||||
if do_d.has(vf_k):
|
||||
var fld := String(do_d.get(vf_k, ""))
|
||||
if fld != "" and not row.has(fld):
|
||||
errs.append({"category": "dangling", "message": "%s do.%s 悬空(行无字段 %s)" % [rid, vf_k, fld]})
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c_w: Dictionary[Variant, Variant] = c_v as Dictionary
|
||||
if c_w.has("value_from"):
|
||||
var wfld := String(c_w.get("value_from", ""))
|
||||
if wfld != "" and not row.has(wfld):
|
||||
errs.append({"category": "dangling", "message": "%s when.value_from 悬空(行无字段 %s)" % [rid, wfld]})
|
||||
|
||||
|
||||
## 组合规则(需求卡片 §3.4 六族——效果类型的必填属性,发射点施加面引用)
|
||||
|
||||
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、
|
||||
|
||||
@@ -52,12 +52,12 @@ func _normalize_override(raw: Dictionary) -> Dictionary[Variant, Variant]:
|
||||
# 调试浮窗百分比后乘;pct=0 时乘法恒等——MC 锚零漂移)----
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + journal.bonus_total("max_hp")
|
||||
var base: int = int(db.combat.hero.max_hp) + int(_base_override.get("max_hp", 0)) + journal.bonus_total("max_hp")
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0)))
|
||||
|
||||
|
||||
func effective_atk() -> int:
|
||||
var base: int = int(db.hero.atk) + int(_base_override.get("atk", 0)) + journal.bonus_total("atk")
|
||||
var base: int = int(db.combat.hero.atk) + int(_base_override.get("atk", 0)) + journal.bonus_total("atk")
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[0])) / 100.0)))
|
||||
|
||||
|
||||
@@ -70,10 +70,10 @@ func effective_ac() -> int:
|
||||
# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线,
|
||||
# 与 hero 表两轴删除前 armor_class=10 行为连续)
|
||||
func _armor_base_ac() -> int:
|
||||
var at: String = armor_type if armor_type != "" else db.forge_default_armor_type()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at)
|
||||
var at: String = armor_type if armor_type != "" else db.crossrun.forge.default_armor_type
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.forge.armor_route_by_type(at)
|
||||
if row.is_empty():
|
||||
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
|
||||
return int(db.combat.hero.armor_class) # 表缺失防御兜底(现役基准)
|
||||
return int(row.get("base_ac", 0))
|
||||
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ func restore_fragment(resume_state: Dictionary) -> Dictionary:
|
||||
|
||||
|
||||
func _find_level_row(level_index: int) -> Dictionary:
|
||||
return db.level_row(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器)
|
||||
return db.run.level.row_by_level_index(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器)
|
||||
|
||||
|
||||
# ---- 图源装配(关卡节点图生成 v1.1——图 = f(参数档, 种子) 纯函数;无模板池、无兜底图)----
|
||||
@@ -327,7 +327,7 @@ func _find_level_row(level_index: int) -> Dictionary:
|
||||
# LevelTable.validate_graph(与产出侧自证同源规格、分工不重叠);违规=起关拒绝,不降级不回退。
|
||||
func _assign_generated_graph(row: Dictionary, seed_val: int) -> bool:
|
||||
var level_index: int = int(row.get("level_index", 0))
|
||||
var params: Dictionary[Variant, Variant] = db.level_gen_params_row(level_index)
|
||||
var params: Dictionary[Variant, Variant] = db.run.level_gen_params.row_by_level_index(level_index)
|
||||
if params.is_empty():
|
||||
push_error("[关卡节点地图] level %d 无参数档行(level_gen_params 缺行——配置错误)" % level_index)
|
||||
return false
|
||||
@@ -433,7 +433,7 @@ func visited_ids() -> Array:
|
||||
|
||||
# ---- 类型可见性读面(信息迷雾 v2.1:近处见类型,远处起雾)----
|
||||
# 可见集 = Boss/终点(type=="boss")∪ 当前候选(下一跳恒可见——生成承诺)∪
|
||||
# {id : |层(id) − 锚层| ≤ N};N = db.level.info_visible_layers(禁硬编码);
|
||||
# {id : |层(id) − 锚层| ≤ N};N = db.run.level.info_visible_layers(禁硬编码);
|
||||
# 锚层 = 当前节点层,入口停点(无当前节点)= 0;层 = 入口 BFS 深度(与呈现层分层同源口径)。
|
||||
# 纯函数零状态:已过节点不豁免(单圈语义,零累积记忆);断点快照零新增字段。
|
||||
func type_visible_ids() -> Array[int]:
|
||||
@@ -443,7 +443,7 @@ func type_visible_ids() -> Array[int]:
|
||||
var cur := current_node()
|
||||
if not cur.is_empty():
|
||||
anchor = int(layers.get(_node_id(cur), 0))
|
||||
var n: int = int(db.level.info_visible_layers)
|
||||
var n: int = int(db.run.level.info_visible_layers)
|
||||
for node in nodes():
|
||||
var nid := _node_id(node)
|
||||
if String(node.get("type", "")) == "boss" \
|
||||
|
||||
+159
-170
@@ -7,6 +7,7 @@ extends Node
|
||||
|
||||
var db: ConfigDb
|
||||
var save: SaveManagerCore
|
||||
var meta_state: MetaStateStore # 存档内元进度数据读写唯一层(K3′ 装配成员——架构 :90 直连)
|
||||
|
||||
|
||||
# MetaProgression 是 autoload(仿 RngService 先例:类名加 Core 后缀、
|
||||
@@ -16,6 +17,8 @@ var save: SaveManagerCore
|
||||
func _init(db_ref: ConfigDb = null, save_ref: SaveManagerCore = null) -> void:
|
||||
db = db_ref
|
||||
save = save_ref
|
||||
if save != null:
|
||||
meta_state = save.meta_state
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -23,31 +26,23 @@ func _ready() -> void:
|
||||
db = get_node("/root/ConfigDB")
|
||||
if save == null:
|
||||
save = get_node("/root/SaveManager")
|
||||
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return save.keystone_balance()
|
||||
meta_state = save.meta_state
|
||||
|
||||
|
||||
# ---- 秘宝解锁服务(解锁混合制,契约 6)----
|
||||
# 解锁条件单一事实源=treasure_items.tres 行内 unlock_keystone_cost/unlock_requires 谓词
|
||||
# (C1 逐件分配锁定表)。
|
||||
|
||||
# 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager)
|
||||
func is_item_unlocked(item_id: String) -> bool:
|
||||
return save.item_unlocked(item_id)
|
||||
|
||||
# (C1 逐件分配锁定表)。钥石余额/道具解锁读数消费方直连 meta.meta_state(纯转发读已随件退役)。
|
||||
|
||||
# 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS;
|
||||
# 词表加键 = 此处增一 match 分支,阈值比较与解锁写档零改动)
|
||||
func _treasure_unlock_progress(key: String) -> int:
|
||||
match key:
|
||||
"adventure_cleared":
|
||||
return save.adventure_cleared()
|
||||
return meta_state.career_state().adventure_cleared
|
||||
"boss_kill_count":
|
||||
return save.boss_kill_count()
|
||||
return meta_state.career_state().boss_kill_count
|
||||
"vision_used":
|
||||
return save.vision_used()
|
||||
return meta_state.career_state().vision_used
|
||||
_:
|
||||
return 0 # 未知计数键恒 0 = 永不解锁(fail-safe 非致命——schema 词表校验兜底,不静默误解锁)
|
||||
|
||||
@@ -55,15 +50,15 @@ func _treasure_unlock_progress(key: String) -> int:
|
||||
# 钥石购买解锁(营地秘宝匣唯一购买写口):仅购买件(行带 unlock_keystone_cost)可购;
|
||||
# 已解锁/余额不足/条件件误购 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币)。
|
||||
func buy_treasure_unlock(item_id: String) -> bool:
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id)
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(item_id)
|
||||
var price := int(arow.get("unlock_keystone_cost", 0))
|
||||
if arow.is_empty() or price < 1 or (arow.get("unlock_requires", []) as Array).size() > 0:
|
||||
return false # 非购买件(条件件/无价行)拒绝
|
||||
if save.item_unlocked(item_id):
|
||||
if meta_state.item_unlocked(item_id):
|
||||
return false # 已解锁重复购买拒绝
|
||||
if not save.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false # 余额不足(零副作用)
|
||||
save.unlock_item(item_id)
|
||||
meta_state.unlock_item(item_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -71,9 +66,9 @@ func buy_treasure_unlock(item_id: String) -> bool:
|
||||
# 全部谓词达标才解锁(多谓词=AND——天眼双条件);幂等:已解锁跳过,重复刷新零重复入账。
|
||||
func refresh_treasure_unlocks() -> Array:
|
||||
var new_unlocks: Array[String] = []
|
||||
for row in db.treasure_attunement_rows():
|
||||
for row in db.crossrun.treasure_items.attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if save.item_unlocked(iid):
|
||||
if meta_state.item_unlocked(iid):
|
||||
continue
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array
|
||||
if ureqs.is_empty():
|
||||
@@ -85,7 +80,7 @@ func refresh_treasure_unlocks() -> Array:
|
||||
met = false
|
||||
break
|
||||
if met:
|
||||
save.unlock_item(iid)
|
||||
meta_state.unlock_item(iid)
|
||||
new_unlocks.append(iid)
|
||||
if not new_unlocks.is_empty():
|
||||
save.write_to_disk()
|
||||
@@ -96,9 +91,9 @@ func refresh_treasure_unlocks() -> Array:
|
||||
# {item_id, display_name, desc, tier, unlocked, mode(keystone|condition|owned), price, reqs:[{key,threshold,progress}]}
|
||||
func treasure_shop_status() -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for row in db.treasure_attunement_rows():
|
||||
for row in db.crossrun.treasure_items.attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var unlocked := save.item_unlocked(iid)
|
||||
var unlocked := meta_state.item_unlocked(iid)
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array
|
||||
var req_view: Array[Variant] = []
|
||||
for req_v in ureqs:
|
||||
@@ -128,18 +123,18 @@ func treasure_shop_status() -> Array:
|
||||
# 等级语义:未解锁/未锻造=0 加值(裸体态);锻造第一级=「获得装备」=首个 +1;逐级递进。
|
||||
|
||||
# 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处)
|
||||
func forge_route_unlocked(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
func _forge_route_unlocked(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
|
||||
if row.is_empty():
|
||||
return false
|
||||
if bool(row.get("initial_owned", false)):
|
||||
return true
|
||||
if save.forge_unlocked_purchases().has(route_id):
|
||||
if meta_state.forge_state().unlocked.has(route_id):
|
||||
return true
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
if prereq != "":
|
||||
var prow: Dictionary[Variant, Variant] = db.forge_route(prereq)
|
||||
if not prow.is_empty() and save.forge_route_level(prereq) >= int(prow.get("cap", 0)):
|
||||
var prow: Dictionary[Variant, Variant] = db.crossrun.forge.route(prereq)
|
||||
if not prow.is_empty() and meta_state.forge_state().levels.get(prereq, 0) >= int(prow.get("cap", 0)):
|
||||
return true
|
||||
return false
|
||||
|
||||
@@ -147,86 +142,60 @@ func forge_route_unlocked(route_id: String) -> bool:
|
||||
# 路线购买(几何价=基础价×150%^已购级数——锚表 20/30/45/67/101;等级+1 即时写盘)。
|
||||
# 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币不写盘)。
|
||||
func forge_buy_level(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
if row.is_empty() or not forge_route_unlocked(route_id):
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
|
||||
if row.is_empty() or not _forge_route_unlocked(route_id):
|
||||
return false
|
||||
var lvl := save.forge_route_level(route_id)
|
||||
var lvl: int = meta_state.forge_state().levels.get(route_id, 0)
|
||||
if lvl >= int(row.get("cap", 0)):
|
||||
return false
|
||||
var price := ForgeTable.level_price(row, lvl)
|
||||
if not save.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.set_forge_route_level(route_id, lvl + 1)
|
||||
meta_state.set_forge_route_level(route_id, lvl + 1)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 一次性解锁价直购(并联双通道②)。已解锁(含练满前一档派生)= 拒绝零收费——两通道互不影响:
|
||||
# 未练满也可直购;直购后练满不重复收(练满通道本就是派生、零收费)。初始持有行无解锁面。
|
||||
func forge_buy_unlock(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
|
||||
if row.is_empty() or bool(row.get("initial_owned", false)):
|
||||
return false
|
||||
if forge_route_unlocked(route_id):
|
||||
if _forge_route_unlocked(route_id):
|
||||
return false
|
||||
var price := int(row.get("unlock_price", 0))
|
||||
if price < 1 or not save.spend_keystone(price):
|
||||
if price < 1 or not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.add_forge_unlock(route_id)
|
||||
meta_state.add_forge_unlock(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照,
|
||||
# 契约 8;冒险中无切换入口=UI 面无路径可达本口)
|
||||
func forge_select_weapon(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(route_id):
|
||||
# 契约 8;冒险中无切换入口=UI 面无路径可达本口)。并组入口:kind ∈ {weapon, armor}。
|
||||
func forge_select(kind: String, route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
|
||||
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(route_id):
|
||||
return false
|
||||
save.set_forge_weapon(route_id)
|
||||
if kind == "weapon":
|
||||
meta_state.set_forge_weapon(route_id)
|
||||
else:
|
||||
meta_state.set_forge_armor(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
func forge_select_armor(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(route_id):
|
||||
return false
|
||||
save.set_forge_armor(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 当前选中武器攻击加值 / 当前甲 AC 加值(路线等级×每级增量——0=裸体态/未锻造合法态)。
|
||||
# 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。
|
||||
func forge_weapon_atk_bonus() -> int:
|
||||
var rid := save.forge_weapon()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid):
|
||||
return 0
|
||||
return save.forge_route_level(rid) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
func forge_armor_ac_bonus() -> int:
|
||||
var rid := save.forge_armor()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid):
|
||||
return 0
|
||||
return save.forge_route_level(rid) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
# 两根类型轴当前生效值(选中路线类型轴;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
|
||||
# 两根类型轴当前生效值({attack, armor} 并组承载;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
|
||||
# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。
|
||||
func hero_attack_type() -> String:
|
||||
var rid := save.forge_weapon()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid):
|
||||
return db.forge_default_attack_type()
|
||||
return String(row.get("type_axis", ""))
|
||||
|
||||
|
||||
func hero_armor_type() -> String:
|
||||
var rid := save.forge_armor()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid):
|
||||
return db.forge_default_armor_type()
|
||||
return String(row.get("type_axis", ""))
|
||||
func hero_type_axes() -> Dictionary:
|
||||
var fs := meta_state.forge_state()
|
||||
var wrow: Dictionary[Variant, Variant] = db.crossrun.forge.route(String(fs["weapon"]))
|
||||
var attack := db.crossrun.forge.default_attack_type
|
||||
if not wrow.is_empty() and String(wrow.get("kind", "")) == "weapon" and _forge_route_unlocked(String(fs["weapon"])):
|
||||
attack = String(wrow.get("type_axis", ""))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.forge.route(String(fs["armor"]))
|
||||
var armor := db.crossrun.forge.default_armor_type
|
||||
if not arow.is_empty() and String(arow.get("kind", "")) == "armor" and _forge_route_unlocked(String(fs["armor"])):
|
||||
armor = String(arow.get("type_axis", ""))
|
||||
return {"attack": attack, "armor": armor}
|
||||
|
||||
|
||||
# ---- 锻造工坊柜台只读投影(呈现层数据源:两区六路线行板;余额联动禁用归视图)----
|
||||
@@ -235,16 +204,16 @@ func hero_armor_type() -> String:
|
||||
# prereq_level, prereq_cap, prereq_maxed}——纯结构面,禁嵌余额判断(视图按 balance 禁用,同秘宝匣先例)。
|
||||
func forge_shop_status() -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for row in db.forge_routes():
|
||||
for row in db.crossrun.forge.route_rows():
|
||||
var rid := String(row.get("route_id", ""))
|
||||
var lvl := save.forge_route_level(rid)
|
||||
var lvl: int = meta_state.forge_state().levels.get(rid, 0)
|
||||
var cap_v := int(row.get("cap", 0))
|
||||
var unlocked := forge_route_unlocked(rid)
|
||||
var unlocked := _forge_route_unlocked(rid)
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
var plvl := save.forge_route_level(prereq) if prereq != "" else 0
|
||||
var pcap := int(db.forge_route(prereq).get("cap", 0)) if prereq != "" else 0
|
||||
var selected := (String(row.get("kind", "")) == "weapon" and save.forge_weapon() == rid) \
|
||||
or (String(row.get("kind", "")) == "armor" and save.forge_armor() == rid)
|
||||
var plvl: int = meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0
|
||||
var pcap := int(db.crossrun.forge.route(prereq).get("cap", 0)) if prereq != "" else 0
|
||||
var selected: bool = (String(row.get("kind", "")) == "weapon" and meta_state.forge_state().weapon == rid) \
|
||||
or (String(row.get("kind", "")) == "armor" and meta_state.forge_state().armor == rid)
|
||||
out.append({
|
||||
"route_id": rid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
@@ -258,7 +227,7 @@ func forge_shop_status() -> Array:
|
||||
"next_price": ForgeTable.level_price(row, lvl) if (unlocked and lvl < cap_v) else 0,
|
||||
"unlock_price": int(row.get("unlock_price", 0)),
|
||||
"prereq_route": prereq,
|
||||
"prereq_name": String(db.forge_route(prereq).get("display_name", "")) if prereq != "" else "",
|
||||
"prereq_name": String(db.crossrun.forge.route(prereq).get("display_name", "")) if prereq != "" else "",
|
||||
"prereq_level": plvl,
|
||||
"prereq_cap": pcap,
|
||||
"prereq_maxed": prereq != "" and plvl >= pcap,
|
||||
@@ -274,10 +243,10 @@ func forge_shop_status() -> Array:
|
||||
# apply_adventure_summary 逐关卡完成关次置位)。本函数只做标记+入账+归档。
|
||||
func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
|
||||
save.mark_first_adventure_done()
|
||||
meta_state.mark_first_adventure_done()
|
||||
var drops: Array[Variant] = (summary.get("keystone_drops", []) as Array).duplicate(true)
|
||||
save.apply_adventure_summary(summary, drops)
|
||||
save.add_vision_used(int(summary.get("vision_used_delta", 0))) # 真视使用计数收束聚合入档(信标网络行为条件)
|
||||
meta_state.apply_adventure_summary(summary, drops)
|
||||
meta_state.add_vision_used(int(summary.get("vision_used_delta", 0))) # 真视使用计数收束聚合入档(信标网络行为条件)
|
||||
var new_unlocks := refresh_treasure_unlocks() # 条件解锁刷新
|
||||
save.write_to_disk()
|
||||
var total := int(summary.get("keystone_total", 0))
|
||||
@@ -299,51 +268,75 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
|
||||
|
||||
func hero_override() -> Dictionary:
|
||||
# 锻造路线加值内联(原 forge_weapon_atk_bonus/forge_armor_ac_bonus 单调用点收编——输出逐字:
|
||||
# 路线等级×每级增量,选中态异常/未解锁防御回退 0)
|
||||
var fs := meta_state.forge_state()
|
||||
var atk_bonus := _forge_axis_bonus(fs, "weapon", "weapon")
|
||||
var ac_bonus := _forge_axis_bonus(fs, "armor", "armor")
|
||||
# 树淬炼 flat 内联(原 tree_stat_flat 单调用点收编——四线 flat 汇总同轮扫描,三轴输出逐字等价)
|
||||
var tree_atk := 0
|
||||
var tree_hp := 0
|
||||
var tree_ac := 0
|
||||
var nodes: Dictionary[Variant, Variant] = meta_state.tree_state()["nodes"]
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
var lvl := int(nodes.get(String(row.get("node_id", "")), 0))
|
||||
if lvl <= 0:
|
||||
continue
|
||||
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
|
||||
tree_atk += int(fx.get("atk", 0)) * lvl
|
||||
tree_hp += int(fx.get("max_hp", 0)) * lvl
|
||||
tree_ac += int(fx.get("armor_class", 0)) * lvl
|
||||
return {
|
||||
"atk": forge_weapon_atk_bonus() + tree_stat_flat("atk"),
|
||||
"max_hp": tree_stat_flat("max_hp"),
|
||||
"armor_class": forge_armor_ac_bonus() + tree_stat_flat("armor_class"),
|
||||
"atk": atk_bonus + tree_atk,
|
||||
"max_hp": tree_hp,
|
||||
"armor_class": ac_bonus + tree_ac,
|
||||
}
|
||||
|
||||
|
||||
# 锻造轴加值原语(选中路线等级×每级增量;选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态)
|
||||
func _forge_axis_bonus(fs: Dictionary, sel_key: String, kind: String) -> int:
|
||||
var rid := String(fs[sel_key])
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(rid):
|
||||
return 0
|
||||
return int(fs["levels"].get(rid, 0)) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
# ---- 天赋树段(设计权威 整合定稿 §1.4 一树双币三脉一脊)----
|
||||
|
||||
# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」)
|
||||
func tree_spent_total() -> int:
|
||||
func _tree_spent_total() -> int:
|
||||
var total := 0
|
||||
for lvl in (save.tree_nodes() as Dictionary).values():
|
||||
for lvl in (meta_state.tree_state().nodes as Dictionary).values():
|
||||
total += int(lvl)
|
||||
return total
|
||||
|
||||
|
||||
# 节点可购校验(行门槛 + 显式前置 + 币 + cap;free 节点=钥匙不经购买,彩蛋=觉醒不经购买)
|
||||
func tree_can_buy(node_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
|
||||
if row.is_empty():
|
||||
return false
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
|
||||
# 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红)
|
||||
if String(row.get("kind", "")) == "easter":
|
||||
return false
|
||||
var lvl := save.tree_level(node_id)
|
||||
if lvl >= int(row.get("cap", 1)):
|
||||
if row.is_empty() or String(row.get("kind", "")) == "easter":
|
||||
return false
|
||||
var nodes: Dictionary[Variant, Variant] = meta_state.tree_state()["nodes"]
|
||||
var lvl := int(nodes.get(node_id, 0))
|
||||
var blocked := lvl >= int(row.get("cap", 1))
|
||||
# 刷新升级节点:cap 主语=Y 账本(单一事实源——树上入口与装配段同 cap 同价,防双账漂移)
|
||||
if node_id == "ks_refresh" and db.meta_attune_refresh_row(save.attune_refresh_level() + 1).is_empty():
|
||||
return false
|
||||
if node_id == "ks_refresh" and db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level + 1).is_empty():
|
||||
blocked = true
|
||||
# 行门槛(一线一节点:脊柱节点第 N 级需全树总投入 ≥4×(N−1))
|
||||
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat":
|
||||
if tree_spent_total() < db.meta_tree.level_gate(lvl + 1):
|
||||
return false
|
||||
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat" and _tree_spent_total() < db.crossrun.meta_tree.level_gate(lvl + 1):
|
||||
blocked = true
|
||||
for req in (row.get("requires", []) as Array):
|
||||
if save.tree_level(String(req)) < 1:
|
||||
return false
|
||||
if int(nodes.get(String(req), 0)) < 1:
|
||||
blocked = true
|
||||
# 等级阈值前置(requires_level {node_id: n}:前置节点等级 ≥ n 才可购;
|
||||
# 现役表零配置=空转零行为影响)
|
||||
var req_lv: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary
|
||||
for req_id in req_lv.keys():
|
||||
if save.tree_level(String(req_id)) < int(req_lv[req_id]):
|
||||
return false
|
||||
return true
|
||||
if int(nodes.get(String(req_id), 0)) < int(req_lv[req_id]):
|
||||
blocked = true
|
||||
return not blocked
|
||||
|
||||
|
||||
# 节点购买(营地树 UI 唯一写口):树全钥石单币。free=工作间钥匙零币点亮
|
||||
@@ -353,13 +346,13 @@ func tree_can_buy(node_id: String) -> bool:
|
||||
func tree_buy(node_id: String) -> bool:
|
||||
if node_id == "ks_refresh":
|
||||
return buy_attune_refresh()
|
||||
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
|
||||
if row.is_empty() or not tree_can_buy(node_id):
|
||||
return false
|
||||
var price := int(row.get("price", 0))
|
||||
if not save.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.set_tree_level(node_id, save.tree_level(node_id) + 1)
|
||||
meta_state.set_tree_level(node_id, meta_state.tree_state().nodes.get(node_id, 0) + 1)
|
||||
_check_easter_egg()
|
||||
return save.write_to_disk()
|
||||
|
||||
@@ -367,45 +360,35 @@ func tree_buy(node_id: String) -> bool:
|
||||
# Fork 切换(营地树 UI——一节点双态:购买解锁组默认 A 态,换态走本口零币;同态重复=零操作;
|
||||
# 未解锁组切换=拒绝——解锁唯一入口=购买面 tree_buy_fork)
|
||||
func tree_toggle_fork(group: String, side: String) -> bool:
|
||||
if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
|
||||
if not db.crossrun.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
|
||||
return false
|
||||
var cur := save.fork_side(group)
|
||||
var cur: String = meta_state.tree_state().fork_sides.get(group, "")
|
||||
if cur == "" or cur == side:
|
||||
return false # 未解锁走购买面;同态零操作
|
||||
save.set_fork_side(group, side)
|
||||
meta_state.set_fork_side(group, side)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# Fork 节点购买入口(一节点双态——购=解锁开关并置默认 A 态;重复购买由 cap 经 can_buy 拦截)
|
||||
func tree_buy_fork(node_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "fork":
|
||||
return tree_buy(node_id)
|
||||
if not tree_can_buy(node_id):
|
||||
return false
|
||||
var price := int(row.get("price", 0))
|
||||
if not save.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.set_tree_level(node_id, 1)
|
||||
save.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
|
||||
meta_state.set_tree_level(node_id, 1)
|
||||
meta_state.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 树效果投影:四线 flat 汇总 + rule 键集(激活 Fork 侧 effects 并入)
|
||||
func tree_stat_flat(key: String) -> int:
|
||||
var total := 0
|
||||
for row in db.tree_rows():
|
||||
var lvl := save.tree_level(String(row.get("node_id", "")))
|
||||
if lvl <= 0:
|
||||
continue
|
||||
total += int((row.get("effects", {}) as Dictionary).get(key, 0)) * lvl
|
||||
return total
|
||||
|
||||
|
||||
# 树规则键投影(激活 Fork 侧 effects 并入)
|
||||
func tree_rule_keys() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var sides: Dictionary[Variant, Variant] = save.fork_sides()
|
||||
for row in db.tree_rows():
|
||||
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
var active := String(sides.get(String(row.get("fork_group", "")), ""))
|
||||
@@ -418,10 +401,15 @@ func tree_rule_keys() -> Array:
|
||||
return out
|
||||
|
||||
|
||||
# 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工)
|
||||
func tree_camp_rows() -> Array:
|
||||
# 营地树面投影(并组入口——{rows, fork_groups};CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工)
|
||||
func tree_camp_status() -> Dictionary:
|
||||
return {"rows": _tree_camp_rows(), "fork_groups": _tree_fork_groups()}
|
||||
|
||||
|
||||
## 树行板投影段(原 tree_camp_rows)
|
||||
func _tree_camp_rows() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
for row in db.tree_rows():
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) == "fork":
|
||||
continue # Fork 行由 fork_groups 段承载
|
||||
out.append({
|
||||
@@ -430,32 +418,33 @@ func tree_camp_rows() -> Array:
|
||||
"vein": String(row.get("vein", "")),
|
||||
"kind": String(row.get("kind", "")),
|
||||
"cap": int(row.get("cap", 1)),
|
||||
"level": save.attune_refresh_level() if String(row.get("node_id", "")) == "ks_refresh" else save.tree_level(String(row.get("node_id", ""))), # 刷新升级行主语=Y 账本(同账路由 V15)
|
||||
"level": meta_state.attune_state().refresh_level if String(row.get("node_id", "")) == "ks_refresh" else meta_state.tree_state().nodes.get(String(row.get("node_id", "")), 0), # 刷新升级行主语=Y 账本(同账路由 V15)
|
||||
"price": int(row.get("price", 0)),
|
||||
"currency": String(row.get("currency", "keystone")),
|
||||
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
|
||||
"gate_text": tree_gate_text(String(row.get("node_id", ""))),
|
||||
"gate_text": _tree_gate_text(String(row.get("node_id", ""))),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# 行板门槛文案(R1 机制位呈现——requires_level 未达标项拼「需 {前置名} Lv{n}」;达标/未配置=空串。
|
||||
# 只呈现未达标项:达标后锁标消失,不做全量前置罗列)
|
||||
func tree_gate_text(node_id: String) -> String:
|
||||
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
|
||||
func _tree_gate_text(node_id: String) -> String:
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
|
||||
var rl: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary
|
||||
var parts: Array[String] = []
|
||||
for req_id in rl.keys():
|
||||
if save.tree_level(String(req_id)) < int(rl[req_id]):
|
||||
var req_row: Dictionary[Variant, Variant] = db.tree_node(String(req_id))
|
||||
if meta_state.tree_state().nodes.get(String(req_id), 0) < int(rl[req_id]):
|
||||
var req_row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(String(req_id))
|
||||
parts.append("需 %s Lv%d" % [String(req_row.get("display_name", req_id)), int(rl[req_id])])
|
||||
return "、".join(parts)
|
||||
|
||||
|
||||
func tree_fork_groups() -> Array:
|
||||
## Fork 行板投影段(原 tree_fork_groups)
|
||||
func _tree_fork_groups() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var sides: Dictionary[Variant, Variant] = save.fork_sides()
|
||||
for row in db.tree_rows():
|
||||
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
var fg := String(row.get("fork_group", ""))
|
||||
@@ -464,7 +453,7 @@ func tree_fork_groups() -> Array:
|
||||
"node_id": String(row.get("node_id", "")),
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"active_side": String(sides.get(fg, "")),
|
||||
"owned": save.tree_level(String(row.get("node_id", ""))) > 0,
|
||||
"owned": meta_state.tree_state().nodes.get(String(row.get("node_id", "")), 0) > 0,
|
||||
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
|
||||
"price": int(row.get("price", 0)),
|
||||
"sides": [
|
||||
@@ -478,16 +467,16 @@ func tree_fork_groups() -> Array:
|
||||
# 彩蛋觉醒检查(特化判定——生命/命中两线拉满 且 同调位 1/2/3 全解锁 → easter_ley 免费点亮;
|
||||
# 不依赖 requires/requires_level 前置形态)
|
||||
func _check_easter_egg() -> void:
|
||||
if save.tree_level("easter_ley") > 0:
|
||||
if meta_state.tree_state().nodes.get("easter_ley", 0) > 0:
|
||||
return
|
||||
for nid in ["sp_hp", "sp_hit"]:
|
||||
var row: Dictionary[Variant, Variant] = db.tree_node(nid)
|
||||
if row.is_empty() or save.tree_level(nid) < int(row.get("cap", 1)):
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(nid)
|
||||
if row.is_empty() or meta_state.tree_state().nodes.get(nid, 0) < int(row.get("cap", 1)):
|
||||
return
|
||||
for i in range(1, 4):
|
||||
if save.tree_level("att_socket%d" % i) < 1:
|
||||
if meta_state.tree_state().nodes.get("att_socket%d" % i, 0) < 1:
|
||||
return
|
||||
save.set_tree_level("easter_ley", 1)
|
||||
meta_state.set_tree_level("easter_ley", 1)
|
||||
|
||||
|
||||
# ---- 同调位账本(装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2)----
|
||||
@@ -498,30 +487,30 @@ func _check_easter_egg() -> void:
|
||||
# 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。
|
||||
# 装配关系写存档持久。
|
||||
func toggle_attunement(item_id: String) -> bool:
|
||||
var equipped: Array[Variant] = save.attunement_equipped()
|
||||
var equipped: Array[Variant] = meta_state.attune_state().equipped
|
||||
if not equipped.has(item_id):
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id)
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(item_id)
|
||||
if arow.is_empty() or String(arow.get("form", "")) == "":
|
||||
return false # 非同调行(过渡/退役行)禁装配
|
||||
if not save.item_unlocked(item_id):
|
||||
if not meta_state.item_unlocked(item_id):
|
||||
return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置)
|
||||
if equipped.size() >= Attunement.slots(db, save):
|
||||
return false # 满位禁选
|
||||
var rank: int = int(db.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99))
|
||||
var rank: int = int(db.crossrun.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99))
|
||||
if rank > equipped.size() + 1:
|
||||
return false # 件档高于将占格档(第 k 格档=k——普通/优秀/稀有递增)
|
||||
equipped.append(item_id)
|
||||
else:
|
||||
equipped.erase(item_id)
|
||||
save.set_attunement_equipped(equipped)
|
||||
meta_state.set_attunement_equipped(equipped)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工)
|
||||
func attune_camp_status() -> Dictionary:
|
||||
var equipped: Array[Variant] = save.attunement_equipped()
|
||||
var equipped: Array[Variant] = meta_state.attune_state().equipped
|
||||
var items: Array[Dictionary] = []
|
||||
for row in db.treasure_attunement_rows():
|
||||
for row in db.crossrun.treasure_items.attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
items.append({
|
||||
"item_id": iid,
|
||||
@@ -530,10 +519,10 @@ func attune_camp_status() -> Dictionary:
|
||||
"tier": String(row.get("tier", "")),
|
||||
"form": String(row.get("form", "")),
|
||||
"equipped": equipped.has(iid),
|
||||
"unlocked": save.item_unlocked(iid),
|
||||
"unlocked": meta_state.item_unlocked(iid),
|
||||
})
|
||||
var lvl := save.attune_refresh_level()
|
||||
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(lvl + 1)
|
||||
var lvl: int = meta_state.attune_state().refresh_level
|
||||
var next_row: Dictionary[Variant, Variant] = db.crossrun.meta.attune_refresh_row(lvl + 1)
|
||||
return {
|
||||
"slots": Attunement.slots(db, save),
|
||||
"equipped": equipped,
|
||||
@@ -549,15 +538,15 @@ func attune_camp_status() -> Dictionary:
|
||||
|
||||
# 每冒险开局刷新次数 Y(当前等级行表值;0 级 = 0 次即无刷新能力——P1 甲初始 0 同构)
|
||||
func attune_refresh_charges() -> int:
|
||||
return int(db.meta_attune_refresh_row(save.attune_refresh_level()).get("refresh_charges", 0))
|
||||
return int(db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level).get("refresh_charges", 0))
|
||||
|
||||
|
||||
# Y 升级购买(直购价 = 下一级行表值 keystone_cost;已满级/余额不足 false 零副作用——buy_forge 同款)
|
||||
func buy_attune_refresh() -> bool:
|
||||
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(save.attune_refresh_level() + 1)
|
||||
var next_row: Dictionary[Variant, Variant] = db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level + 1)
|
||||
if next_row.is_empty():
|
||||
return false
|
||||
if not save.spend_keystone(int(next_row.get("keystone_cost", 0))):
|
||||
if not meta_state.spend_keystone(int(next_row.get("keystone_cost", 0))):
|
||||
return false
|
||||
save.set_attune_refresh_level(save.attune_refresh_level() + 1)
|
||||
meta_state.set_attune_refresh_level(meta_state.attune_state().refresh_level + 1)
|
||||
return save.write_to_disk()
|
||||
@@ -0,0 +1,176 @@
|
||||
## 存档内元进度数据件(D3 拆件——自 save_manager 收编元进度族):存档内元进度数据的读写唯一层。
|
||||
## 数据读写原语族(写口 13)+只读投影(读 6——tree/forge/attune/career 四域值拷贝出户);
|
||||
## 业务规则与写口编排归 MetaProgression,本件只承载键域读写(照单承载,零业务判定)。
|
||||
## 零 _data 副本零缓存——全部读写经注入 save 的内存数据模型单点。
|
||||
class_name MetaStateStore
|
||||
extends RefCounted
|
||||
|
||||
var _save # SaveManagerCore 注入引用(_data 唯一取数点)
|
||||
|
||||
|
||||
func _init(save_ref) -> void:
|
||||
_save = save_ref
|
||||
|
||||
|
||||
# ---- 写口原语族(13)----
|
||||
|
||||
# 钥石消费唯一口:余额不足拒绝且零副作用(钥石只增不减——消费除外,元进度验收判据)
|
||||
func spend_keystone(n: int) -> bool:
|
||||
if keystone_balance() < n:
|
||||
return false
|
||||
_save._data["meta"]["keystone_balance"] = keystone_balance() - n
|
||||
return true
|
||||
|
||||
|
||||
func unlock_item(item_id: String) -> void:
|
||||
_save._data["meta"]["item_unlocks"][item_id] = true
|
||||
|
||||
|
||||
func set_tree_level(node_id: String, level: int) -> void:
|
||||
var tn: Dictionary[Variant, Variant] = _save._data["meta"].get("tree_nodes", {}) as Dictionary
|
||||
tn[node_id] = maxi(0, level)
|
||||
_save._data["meta"]["tree_nodes"] = tn
|
||||
|
||||
|
||||
func set_fork_side(group: String, side: String) -> void:
|
||||
var fs: Dictionary[Variant, Variant] = _save._data["meta"].get("fork_sides", {}) as Dictionary
|
||||
fs[group] = side
|
||||
_save._data["meta"]["fork_sides"] = fs
|
||||
|
||||
|
||||
func set_forge_route_level(route_id: String, level: int) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var lv: Dictionary[Variant, Variant] = dom.get("levels", {}) as Dictionary
|
||||
lv[route_id] = maxi(0, level)
|
||||
dom["levels"] = lv
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func add_forge_unlock(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var un: Array[Variant] = dom.get("unlocked", []) as Array
|
||||
if not un.has(route_id):
|
||||
un.append(route_id)
|
||||
dom["unlocked"] = un
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func set_forge_weapon(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["weapon"] = route_id
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func set_forge_armor(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["armor"] = route_id
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func set_attunement_equipped(item_ids: Array) -> void:
|
||||
var out: Array[Variant] = []
|
||||
for v in item_ids:
|
||||
out.append(String(v))
|
||||
_save._data["meta"]["attunement_equipped"] = out
|
||||
|
||||
|
||||
func set_attune_refresh_level(n: int) -> void:
|
||||
_save._data["meta"]["attune_refresh_level"] = maxi(0, n)
|
||||
|
||||
|
||||
# 真视类工具累计使用入账(写入唯一经本口冒险收束聚合,受理计数点=battle 层经冒险层聚合)
|
||||
func add_vision_used(n: int) -> void:
|
||||
_save._data["meta"]["vision_used"] = maxi(0, career_state()["vision_used"] + maxi(0, n))
|
||||
|
||||
|
||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||
# 钥石入账(summary.keystone_total——逐杀掉落已在冒险内计入,boss_drops 参数为
|
||||
# 摘要归档副本不重复入账)+ 逐关卡完成关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计
|
||||
# (同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附
|
||||
# keystone_drops 副本——复盘可归因)+ 生涯终身计数自增(总局数开局即计:
|
||||
# 三结局同经本口恰计一次;冒险成功局数按 summary.cleared 判定三关全通才计 1。终身计数写入
|
||||
# 单口=本函数,禁第二写入点)。
|
||||
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
|
||||
_save._data["meta"]["adventure_total"] = career_state()["adventure_total"] + 1
|
||||
if bool(summary.get("cleared", false)):
|
||||
_save._data["meta"]["adventure_cleared"] = career_state()["adventure_cleared"] + 1
|
||||
_add_keystone(int(summary.get("keystone_total", 0)))
|
||||
var cleared := int(summary.get("levels_cleared", 0))
|
||||
for li in range(1, cleared + 1):
|
||||
var key := str(li)
|
||||
_save._data["meta"]["level_clear_counts"][key] = int(_save._data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
||||
_save._data["meta"]["boss_first_kill"][key] = true
|
||||
_save._data["meta"]["boss_kill_count"] = career_state()["boss_kill_count"] + cleared # Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复)
|
||||
var entry: Dictionary[Variant, Variant] = summary.duplicate(true)
|
||||
entry["boss_drops"] = boss_drops.duplicate(true)
|
||||
var arr: Array[Variant] = _save._data["meta"]["recent_summaries"]
|
||||
arr.push_front(entry)
|
||||
while arr.size() > _save.SUMMARY_CAP:
|
||||
arr.pop_back()
|
||||
|
||||
|
||||
func mark_first_adventure_done() -> void:
|
||||
_save._data["meta"]["first_adventure_done"] = true
|
||||
|
||||
|
||||
# 钥石入账原语(生产消费零——apply_adventure_summary 专用;测量面经 _ 前缀直调)
|
||||
func _add_keystone(n: int) -> void:
|
||||
_save._data["meta"]["keystone_balance"] = keystone_balance() + n
|
||||
|
||||
|
||||
# ---- 只读投影族(6——值拷贝出户,禁碰内部结构契约同款)----
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return int(_save._data["meta"]["keystone_balance"])
|
||||
|
||||
|
||||
func item_unlocked(item_id: String) -> bool:
|
||||
return _save._data["meta"]["item_unlocks"].has(item_id)
|
||||
|
||||
|
||||
## 天赋树域只读投影:{nodes: node_id→已购等级, fork_sides: fork_group→"a"|"b"}
|
||||
func tree_state() -> Dictionary:
|
||||
return {
|
||||
"nodes": (_save._data["meta"].get("tree_nodes", {}) as Dictionary).duplicate(),
|
||||
"fork_sides": (_save._data["meta"].get("fork_sides", {}) as Dictionary).duplicate(),
|
||||
}
|
||||
|
||||
|
||||
## 锻造域只读投影:{levels: 六路线等级, unlocked: 直购清单, weapon/armor: 当前选中}
|
||||
## (解锁态不落档——派生判定单点在 MetaProgression.forge_route_unlocked)
|
||||
func forge_state() -> Dictionary:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
return {
|
||||
"levels": (dom.get("levels", {}) as Dictionary).duplicate(),
|
||||
"unlocked": (dom.get("unlocked", []) as Array).duplicate(),
|
||||
"weapon": String(dom.get("weapon", "sword")),
|
||||
"armor": String(dom.get("armor", "light")),
|
||||
}
|
||||
|
||||
|
||||
## 同调域只读投影:{equipped: 装配 item_id 有序数组(装配序即呈现序), refresh_level: Y 升级等级}
|
||||
func attune_state() -> Dictionary:
|
||||
return {
|
||||
"equipped": (_save._data["meta"].get("attunement_equipped", []) as Array).duplicate(),
|
||||
"refresh_level": int(_save._data["meta"].get("attune_refresh_level", 0)),
|
||||
}
|
||||
|
||||
|
||||
## 生涯域只读投影:{clear_counts, boss_first_kill, boss_kill_count, adventure_total,
|
||||
## adventure_cleared, vision_used, recent_summaries, first_adventure_done}
|
||||
func career_state() -> Dictionary:
|
||||
return {
|
||||
"clear_counts": (_save._data["meta"]["level_clear_counts"] as Dictionary).duplicate(),
|
||||
"boss_first_kill": (_save._data["meta"]["boss_first_kill"] as Dictionary).duplicate(),
|
||||
"boss_kill_count": int(_save._data["meta"].get("boss_kill_count", 0)),
|
||||
"adventure_total": int(_save._data["meta"].get("adventure_total", 0)),
|
||||
"adventure_cleared": int(_save._data["meta"].get("adventure_cleared", 0)),
|
||||
"vision_used": int(_save._data["meta"].get("vision_used", 0)),
|
||||
"recent_summaries": (_save._data["meta"]["recent_summaries"] as Array).duplicate(true),
|
||||
"first_adventure_done": bool(_save._data["meta"].get("first_adventure_done", true)),
|
||||
}
|
||||
|
||||
|
||||
# 锻造域取数(本件内部单点)
|
||||
func _forge_domain() -> Dictionary:
|
||||
return _save._data["meta"].get("forge_routes", {}) as Dictionary
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc5perm32w2yp
|
||||
@@ -304,15 +304,22 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
if was_sealed:
|
||||
_seals.erase(c)
|
||||
var reveal_order: Array[Variant] = _chain_reveal_order(c, eff_seals)
|
||||
_reveal_chain(c, reveal_order, was_sealed)
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
_emit_round_end_seals_and_payload()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
## 连锁揭示域:首格+连锁新格揭示(BFS 规范序——cell_revealed 发射序即链序,整体随段搬迁)。
|
||||
## 格面资源资格:chain/dig 恒 true——安全格与危险格一视同仁(危险格亦携带,
|
||||
## 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
|
||||
## dug_under_seal = 首格带封印直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越封印)
|
||||
func _reveal_chain(c: Vector2i, reveal_order: Array, was_sealed: bool) -> void:
|
||||
for rc in reveal_order:
|
||||
if _revealed.has(rc):
|
||||
continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|)
|
||||
var is_mine := _board.is_mine(rc)
|
||||
var channel := "dig" if rc == c else "chain"
|
||||
# 格面资源资格:chain/dig 恒 true——安全格与危险格一视同仁(危险格亦携带,
|
||||
# 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
|
||||
# dug_under_seal = 首格带封印直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越封印)
|
||||
if is_mine:
|
||||
if _board.is_mine(rc):
|
||||
_revealed[rc] = {"kind": "mine", "digit": -1}
|
||||
cell_revealed.emit({
|
||||
"cell": rc, "kind": "mine", "digit": -1,
|
||||
@@ -326,9 +333,6 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
"variant_ids": _board.variant_ids(rc), "channel": channel, "resource": true,
|
||||
"dug_under_seal": was_sealed and rc == c,
|
||||
})
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
_emit_round_end_seals_and_payload()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
func _board_face_dig(c: Vector2i) -> String:
|
||||
@@ -492,7 +496,7 @@ func _emit_seals_batch() -> void:
|
||||
|
||||
func _emit_round_end_seals_and_payload() -> void:
|
||||
_emit_seals_batch()
|
||||
var payload: Dictionary[Variant, Variant] = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约)
|
||||
var payload: Dictionary[Variant, Variant] = action_economy.last_round_end() # 逐字段取自 行动经济(读取权 = 接口契约;属性→方法 K5)
|
||||
round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")})
|
||||
|
||||
|
||||
|
||||
+219
-124
@@ -66,6 +66,14 @@ func _setup_and_build(params: Dictionary, seed_val: int) -> Dictionary:
|
||||
|
||||
## 参数档可行性判定(执行边界③:参数无解=配置错误)——域校验外加深度预算与战斗保底可放性。
|
||||
static func check_params(params: Dictionary) -> Array:
|
||||
var errs: Array[Dictionary] = _cp_domain(params)
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
return _cp_structural(params)
|
||||
|
||||
|
||||
## 域校验段:五旋钮值域/岔口去路数锁值/配比四类合计 1
|
||||
static func _cp_domain(params: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L := int(params.get("layers", 0))
|
||||
var e := int(params.get("exit_count", 0))
|
||||
@@ -89,8 +97,16 @@ static func check_params(params: Dictionary) -> Array:
|
||||
sum += float(params.get(rk, -1.0))
|
||||
if not is_equal_approx(sum, 1.0):
|
||||
errs.append({"category": "domain", "message": "配比四类合计 %f ≠ 1" % sum})
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
return errs
|
||||
|
||||
|
||||
## 结构可行性段:主链段容量与战斗保底可放性(多出口按区域签名累加可放槽)
|
||||
static func _cp_structural(params: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L := int(params.get("layers", 0))
|
||||
var e := int(params.get("exit_count", 0))
|
||||
var en := int(params.get("entry_count", 0))
|
||||
var bf := int(params.get("battle_floor", -1))
|
||||
var t := 0 if e == 1 else _trunk_len(L, e)
|
||||
if e > 1 and t < 1:
|
||||
errs.append({"category": "structural", "message": "层深 %d 与出口数 %d 无解(主链段放不下归属区域)" % [L, e]})
|
||||
@@ -116,11 +132,33 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
return errs
|
||||
var by_id := {}
|
||||
var indeg := {}
|
||||
if not _vo_node_fields(nodes, errs, by_id, indeg):
|
||||
return errs
|
||||
if not _vo_edges(nodes, by_id, indeg, errs):
|
||||
return errs
|
||||
var flow := _vo_flow_facts(nodes, indeg, errs)
|
||||
if not flow["ok"]:
|
||||
return errs
|
||||
if not _vo_depth_reach(nodes, by_id, flow["entries"], flow["terminals"], errs):
|
||||
return errs
|
||||
_vo_shop_counts(nodes, flow["terminals"], errs)
|
||||
_vo_terminal_shops(nodes, flow["terminals"], errs)
|
||||
if not _all_paths_one_shop(by_id, flow["entries"], flow["terminals"]):
|
||||
errs.append({"category": "structural", "message": "存在入口→终点路径穿店数 ≠ 1(必经恰一店破)"})
|
||||
_check_fork_rules(nodes, by_id, errs)
|
||||
_vo_terminal_depths(flow["terminals"], by_id, errs)
|
||||
_vo_width_rules(nodes, errs)
|
||||
_vo_params_check(params, nodes, by_id, [flow["entries"], flow["terminals"]], errs)
|
||||
return errs
|
||||
|
||||
|
||||
## 阶段一·节点字段域:字段齐备/句柄唯一/类型词表/深度非负(by_id/indeg 引用填充)
|
||||
func _vo_node_fields(nodes: Array, errs: Array, by_id: Dictionary, indeg: Dictionary) -> bool:
|
||||
for n in nodes:
|
||||
for k in ["id", "type", "depth", "next"]:
|
||||
if not (n as Dictionary).has(k):
|
||||
errs.append({"category": "structural", "message": "节点字段缺失: %s" % k})
|
||||
return errs
|
||||
return false
|
||||
var id: int = int(n["id"])
|
||||
if by_id.has(id):
|
||||
errs.append({"category": "dup_key", "message": "节点句柄重复: %d" % id})
|
||||
@@ -130,8 +168,11 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
if int(n["depth"]) < 0:
|
||||
errs.append({"category": "domain", "message": "节点深度为负: %d" % n["depth"]})
|
||||
indeg[id] = 0
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
return errs.is_empty()
|
||||
|
||||
|
||||
## 阶段二·边域:出边悬空与环/回边判据(indeg 引用累计)
|
||||
func _vo_edges(nodes: Array, by_id: Dictionary, indeg: Dictionary, errs: Array) -> bool:
|
||||
for n in nodes:
|
||||
for oid in (n["next"] as Array):
|
||||
if not by_id.has(int(oid)):
|
||||
@@ -141,9 +182,11 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
errs.append({"category": "structural", "message": "连线未朝前进方向(目标深度 %d 不严格大于源 %d)——环/回边" % [
|
||||
int(by_id[int(oid)]["depth"]), int(n["depth"])]})
|
||||
indeg[int(oid)] = int(indeg[int(oid)]) + 1
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
return errs.is_empty()
|
||||
|
||||
|
||||
## 阶段三·流事实域:入口集(入度 0)/终点集(无出边)与 Boss 终点判据、去路数形态
|
||||
func _vo_flow_facts(nodes: Array, indeg: Dictionary, errs: Array) -> Dictionary:
|
||||
var entries := []
|
||||
var terminals := []
|
||||
for n in nodes:
|
||||
@@ -159,11 +202,12 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
errs.append({"category": "structural", "message": "Boss 节点带出边: %d" % id0})
|
||||
elif outs.size() != 1 and not (outs.size() >= 2 and outs.size() <= 3):
|
||||
errs.append({"category": "domain", "message": "节点 %d 去路数 %d 非〔恰一条或岔口 2–3〕" % [id0, outs.size()]})
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
return {"entries": entries, "terminals": terminals, "ok": errs.is_empty()}
|
||||
|
||||
# 深度=自入口最长路径步数(契约 2 赋值语义——出边严格前进的无环判据成立的前提)
|
||||
var longest := _longest_paths(nodes, by_id, entries, indeg)
|
||||
|
||||
## 阶段四·深度与可达域:深度=自入口最长路径步数(契约 2)+全节点可达某终点
|
||||
func _vo_depth_reach(nodes: Array, by_id: Dictionary, entries: Array, terminals: Array, errs: Array) -> bool:
|
||||
var longest := _longest_paths(nodes, by_id, entries)
|
||||
for n in nodes:
|
||||
var id1: int = int(n["id"])
|
||||
if int(n["depth"]) != int(longest.get(id1, -1)):
|
||||
@@ -175,16 +219,21 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
for n in nodes:
|
||||
if not reaches.has(int(n["id"])):
|
||||
errs.append({"category": "structural", "message": "节点 %d 不可达任何终点(断头/孤儿)" % n["id"]})
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
return errs.is_empty()
|
||||
|
||||
# 摆放约束①:每终点前一位恰一店、商店总数=终点数、终点前一层去路集合恰含一个终点
|
||||
|
||||
## 摆放约束①前半·商店总数:商店总数=终点数(商店数=出口数已锁)
|
||||
func _vo_shop_counts(nodes: Array, terminals: Array, errs: Array) -> void:
|
||||
var shop_count := 0
|
||||
for n in nodes:
|
||||
if String(n["type"]) == "shop":
|
||||
shop_count += 1
|
||||
if shop_count != terminals.size():
|
||||
errs.append({"category": "structural", "message": "商店总数 %d ≠ 终点数 %d(商店数=出口数已锁)" % [shop_count, terminals.size()]})
|
||||
|
||||
|
||||
## 摆放约束①后半·终点前驱:每终点前一位恰一店且为唯一前驱、终点前一层去路集合恰含一个终点
|
||||
func _vo_terminal_shops(nodes: Array, terminals: Array, errs: Array) -> void:
|
||||
for tid in terminals:
|
||||
var preds := []
|
||||
for n in nodes:
|
||||
@@ -204,16 +253,19 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
if t_hits != 1:
|
||||
errs.append({"category": "structural", "message": "终点 %d 前一层去路集合含终点 %d 个(归属锁定破)" % [tid, t_hits]})
|
||||
|
||||
if not _all_paths_one_shop(by_id, entries, terminals):
|
||||
errs.append({"category": "structural", "message": "存在入口→终点路径穿店数 ≠ 1(必经恰一店破)"})
|
||||
_check_fork_rules(nodes, by_id, errs)
|
||||
|
||||
## 计数规则①·多终点深度互异(推论条款 A——定位信息只有两样)
|
||||
func _vo_terminal_depths(terminals: Array, by_id: Dictionary, errs: Array) -> void:
|
||||
var tdepths := {}
|
||||
for tid in terminals:
|
||||
var dd: int = int(by_id[tid]["depth"])
|
||||
if tdepths.has(dd):
|
||||
errs.append({"category": "structural", "message": "多终点同深度 %d(推论条款 A 破——定位信息只有两样)" % dd})
|
||||
tdepths[dd] = true
|
||||
|
||||
|
||||
## 计数规则②·宽度上限:同深度节点数 ≤ 分叉生长上限 4(禁平行层摆放)
|
||||
func _vo_width_rules(nodes: Array, errs: Array) -> void:
|
||||
var per_depth := {}
|
||||
for n in nodes:
|
||||
var d2: int = int(n["depth"])
|
||||
@@ -222,23 +274,28 @@ func validate_output(graph: Dictionary, params: Dictionary = {}) -> Array:
|
||||
if int(per_depth[d3]) > MAX_WIDTH:
|
||||
errs.append({"category": "structural", "message": "深度 %d 节点数 %d 超分叉生长上限 4(平行层摆放)" % [d3, per_depth[d3]]})
|
||||
|
||||
|
||||
## 计数规则③·参数档对照:战斗保底/入口数/终点数/层高(params 为空时跳过)
|
||||
func _vo_params_check(params: Dictionary, nodes: Array, by_id: Dictionary, flow_ends: Array, errs: Array) -> void:
|
||||
var entries: Array = flow_ends[0]
|
||||
var terminals: Array = flow_ends[1]
|
||||
var battles := 0
|
||||
for n in nodes:
|
||||
if String(n["type"]) == "battle":
|
||||
battles += 1
|
||||
if not params.is_empty():
|
||||
if battles < int(params.get("battle_floor", 0)):
|
||||
errs.append({"category": "domain", "message": "战斗节点数 %d 低于保底 %d" % [battles, params.get("battle_floor", 0)]})
|
||||
if entries.size() != int(params.get("entry_count", entries.size())):
|
||||
errs.append({"category": "structural", "message": "入口数 %d ≠ 参数档 entry_count %d" % [entries.size(), params.get("entry_count", 0)]})
|
||||
if terminals.size() != int(params.get("exit_count", terminals.size())):
|
||||
errs.append({"category": "structural", "message": "终点数 %d ≠ 参数档 exit_count %d" % [terminals.size(), params.get("exit_count", 0)]})
|
||||
var maxd := 0
|
||||
for tid in terminals:
|
||||
maxd = maxi(maxd, int(by_id[tid]["depth"]))
|
||||
if maxd != int(params.get("layers", maxd)):
|
||||
errs.append({"category": "domain", "message": "层高(最长路径步数)%d ≠ 参数档 layers %d" % [maxd, params.get("layers", 0)]})
|
||||
return errs
|
||||
if params.is_empty():
|
||||
return
|
||||
if battles < int(params.get("battle_floor", 0)):
|
||||
errs.append({"category": "domain", "message": "战斗节点数 %d 低于保底 %d" % [battles, params.get("battle_floor", 0)]})
|
||||
if entries.size() != int(params.get("entry_count", entries.size())):
|
||||
errs.append({"category": "structural", "message": "入口数 %d ≠ 参数档 entry_count %d" % [entries.size(), params.get("entry_count", 0)]})
|
||||
if terminals.size() != int(params.get("exit_count", terminals.size())):
|
||||
errs.append({"category": "structural", "message": "终点数 %d ≠ 参数档 exit_count %d" % [terminals.size(), params.get("exit_count", 0)]})
|
||||
var maxd := 0
|
||||
for tid in terminals:
|
||||
maxd = maxi(maxd, int(by_id[tid]["depth"]))
|
||||
if maxd != int(params.get("layers", maxd)):
|
||||
errs.append({"category": "domain", "message": "层高(最长路径步数)%d ≠ 参数档 layers %d" % [maxd, params.get("layers", 0)]})
|
||||
|
||||
|
||||
# ---- 构型(长图算法 HOW 归实现——D1:28;合法性由 validate_output 强制)----
|
||||
@@ -252,28 +309,11 @@ func _build(rg: RandomNumberGenerator) -> Dictionary:
|
||||
var entries: Array[int] = []
|
||||
for i in range(en):
|
||||
entries.append(_mk("battle", 0))
|
||||
var prevs: Array[int] = entries.duplicate()
|
||||
for d in range(1, t + 1):
|
||||
var nid := _mk("battle", d)
|
||||
for p in prevs:
|
||||
(_by_id[p]["next"] as Array).append(nid)
|
||||
prevs = [nid]
|
||||
var prevs: Array = _build_trunk(t, entries.duplicate())
|
||||
|
||||
var terminals: Array[int] = []
|
||||
for r in range(e):
|
||||
var bdepth := L - r
|
||||
var bid := _mk("boss", bdepth)
|
||||
var sid := _mk("shop", bdepth - 1)
|
||||
(_by_id[sid]["next"] as Array).append(bid)
|
||||
var cur := sid
|
||||
var sig := String(REGION_SIGNATURES[r]) if e > 1 else "battle"
|
||||
for d in range(bdepth - 2, t, -1):
|
||||
var fid := _mk(sig, d)
|
||||
(_by_id[fid]["next"] as Array).append(cur)
|
||||
cur = fid
|
||||
terminals.append(bid)
|
||||
for p in prevs:
|
||||
(_by_id[p]["next"] as Array).append(cur)
|
||||
_build_region(r, e, L, prevs, terminals)
|
||||
# e>1 时 prevs 收束为主链段末节点一处 ⇒ 该节点即归属大岔口(各区域互不连达);
|
||||
# e==1 时 prevs 为入口数组,直接接单区域头(无大岔口——L1 出口恒 1)。
|
||||
|
||||
@@ -281,41 +321,61 @@ func _build(rg: RandomNumberGenerator) -> Dictionary:
|
||||
_assign_types(rg, e)
|
||||
|
||||
var nodes: Array[Dictionary] = []
|
||||
for n in _pool:
|
||||
nodes.append(n)
|
||||
nodes.assign(_pool)
|
||||
return {"nodes": nodes, "entries": entries, "terminals": terminals}
|
||||
|
||||
|
||||
## 主链段(深度 1..t 逐层单链,主链段零 RNG):返回链末节点集(e>1 时收束为一处即归属大岔口)
|
||||
func _build_trunk(t: int, prevs: Array) -> Array:
|
||||
for d in range(1, t + 1):
|
||||
var nid := _mk("battle", d)
|
||||
for p in prevs:
|
||||
(_by_id[p]["next"] as Array).append(nid)
|
||||
prevs = [nid]
|
||||
return prevs
|
||||
|
||||
|
||||
## 归属区域段(每出口一条支路:Boss 终点+店+签名填充链;区域段零 RNG):
|
||||
## Boss 深度 = L−r、店在其前一位、签名链自店向主链段方向回铺,支路头接入 prevs(归属大岔口)
|
||||
func _build_region(r: int, e: int, L: int, prevs: Array, terminals: Array) -> void:
|
||||
var t := 0 if e == 1 else _trunk_len(L, e)
|
||||
var bdepth := L - r
|
||||
var bid := _mk("boss", bdepth)
|
||||
var sid := _mk("shop", bdepth - 1)
|
||||
(_by_id[sid]["next"] as Array).append(bid)
|
||||
var cur := sid
|
||||
var sig := String(REGION_SIGNATURES[r]) if e > 1 else "battle"
|
||||
for d in range(bdepth - 2, t, -1):
|
||||
var fid := _mk(sig, d)
|
||||
(_by_id[fid]["next"] as Array).append(cur)
|
||||
cur = fid
|
||||
terminals.append(bid)
|
||||
for p in prevs:
|
||||
(_by_id[p]["next"] as Array).append(cur)
|
||||
|
||||
|
||||
## 岔口布置:主链段(深度 ≤ t-2)或单出口图链上(深度 ≤ L-3)的节点扩为 2–3 去路岔口;
|
||||
## 侧支节点与主去路同深(保持「深度=自入口最长路径步数」不变量),两支各自汇入主去路的后继,
|
||||
## 于是支路互斥段={主去路节点} 与 {侧支节点}——两支类型互异即同时满足「下一跳互异」与「各有独有类型」。
|
||||
func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void:
|
||||
var wanted := clampi(int(round(float(_params["branch_density"]) * float(L))), 1, 24)
|
||||
var anchors := []
|
||||
for n in _pool:
|
||||
if int(n["depth"]) > (t - 2 if e > 1 else L - 3):
|
||||
continue
|
||||
if (n["next"] as Array).size() != 1:
|
||||
continue
|
||||
anchors.append(n)
|
||||
var anchors := _collect_fork_anchors(L, t, e)
|
||||
anchors = _shuffle(rg, anchors)
|
||||
var selected := {} # 已扩岔节点 id
|
||||
var succ_selected := {} # 已扩岔节点的主去路 id(禁再作锚点)
|
||||
for a in anchors:
|
||||
if wanted <= 0:
|
||||
break
|
||||
var node = a
|
||||
var nid := int(node["id"])
|
||||
if (node["next"] as Array).size() != 1:
|
||||
var nid := int(a["id"])
|
||||
if (a["next"] as Array).size() != 1:
|
||||
continue # 同一节点不重复扩岔(去路数上限 3)
|
||||
var v = _by_id[int((node["next"] as Array)[0])]
|
||||
var v = _by_id[int((a["next"] as Array)[0])]
|
||||
var vid := int(v["id"])
|
||||
# 相邻两层不得同时扩岔:若主去路 v 也是岔口,v 的侧支会落进本岔口主支路的互斥段,
|
||||
# 使侧支自己的类型不再独有(契约 5③)——两个方向都要拦。
|
||||
if selected.has(vid) or succ_selected.has(nid):
|
||||
continue
|
||||
var v_outs = v["next"]
|
||||
if v_outs.is_empty():
|
||||
if (v["next"] as Array).is_empty():
|
||||
continue # 主去路无后继(无可汇入点)——该锚点弃用
|
||||
var extra := 2 if (int(_params["fork_ways_max"]) >= 3 and rg.randf() < 0.35) else 1
|
||||
var made := 0
|
||||
@@ -323,8 +383,8 @@ func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void:
|
||||
if _depth_count(int(v["depth"])) >= MAX_WIDTH:
|
||||
break
|
||||
var wid := _mk("battle", int(v["depth"]))
|
||||
(node["next"] as Array).append(wid)
|
||||
(_by_id[wid]["next"] as Array).append(int(v_outs[0]))
|
||||
(a["next"] as Array).append(wid)
|
||||
(_by_id[wid]["next"] as Array).append(int((v["next"] as Array)[0]))
|
||||
_detour_heads.append(_by_id[wid])
|
||||
_detour_ids[wid] = true
|
||||
made += 1
|
||||
@@ -334,6 +394,18 @@ func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void:
|
||||
wanted -= 1
|
||||
|
||||
|
||||
## 锚收集段(零 RNG):主链段(深度 ≤ t-2)或单出口图链上(深度 ≤ L-3)的去路恰一节点
|
||||
func _collect_fork_anchors(L: int, t: int, e: int) -> Array:
|
||||
var anchors := []
|
||||
for n in _pool:
|
||||
if int(n["depth"]) > (t - 2 if e > 1 else L - 3):
|
||||
continue
|
||||
if (n["next"] as Array).size() != 1:
|
||||
continue
|
||||
anchors.append(n)
|
||||
return anchors
|
||||
|
||||
|
||||
# 确定性洗牌(Fisher-Yates,取数只经注入的纯流 RNG——Array.shuffle() 走引擎全局随机、
|
||||
# 破「同(种子,参数档)⇒ 同图」的纯函数复现前提,禁用于生成器)
|
||||
func _shuffle(rg: RandomNumberGenerator, arr: Array) -> Array:
|
||||
@@ -358,31 +430,42 @@ func _depth_count(d: int) -> int:
|
||||
## 单出口图的配比填余(摆放优先、配比填余、战斗保底兜底——§8.1 次序),再统一修岔口下一跳互异。
|
||||
func _assign_types(rg: RandomNumberGenerator, e: int) -> void:
|
||||
if e == 1:
|
||||
var free: Array[Dictionary] = []
|
||||
var chain_battles := 0
|
||||
for n in _pool:
|
||||
if String(n["type"]) == "shop" or String(n["type"]) == "boss":
|
||||
continue
|
||||
if _detour_ids.has(int(n["id"])):
|
||||
continue # 侧链节点类型由岔口互异修型决定,不占配比填余面
|
||||
if int(n["depth"]) == 1:
|
||||
continue # 区域首节点留战斗——配比填余不破坏岔口下一跳互异的基准形
|
||||
if String(n["type"]) == "battle":
|
||||
chain_battles += 1
|
||||
free.append(n)
|
||||
free = _shuffle(rg, free)
|
||||
var want := int(round(float(_pool.size()) * (float(_params["reward_ratio"]) + float(_params["rest_ratio"]))))
|
||||
var floor_n := int(_params["battle_floor"])
|
||||
var rest_first := rg.randf() < 0.5
|
||||
for i in range(want):
|
||||
if i >= free.size():
|
||||
break
|
||||
# 战斗保底优先:链上战斗数降到保底下限即停(侧链修型可能再让出战斗位,故保底只看链上)
|
||||
if chain_battles - 1 < floor_n:
|
||||
break
|
||||
var node = free[i]
|
||||
node["type"] = "rest" if ((i % 2 == 0) == rest_first) else "reward"
|
||||
chain_battles -= 1
|
||||
_fill_ratios(rg)
|
||||
_fix_detour_heads()
|
||||
|
||||
|
||||
## 配比填余段(单出口图;shuffle+randf 取号序整体搬迁——战斗保底优先、侧链不占填余面):
|
||||
## 摆放优先、配比填余、战斗保底兜底——§8.1 次序
|
||||
func _fill_ratios(rg: RandomNumberGenerator) -> void:
|
||||
var free: Array[Dictionary] = []
|
||||
var chain_battles := 0
|
||||
for n in _pool:
|
||||
if String(n["type"]) == "shop" or String(n["type"]) == "boss":
|
||||
continue
|
||||
if _detour_ids.has(int(n["id"])):
|
||||
continue # 侧链节点类型由岔口互异修型决定,不占配比填余面
|
||||
if int(n["depth"]) == 1:
|
||||
continue # 区域首节点留战斗——配比填余不破坏岔口下一跳互异的基准形
|
||||
if String(n["type"]) == "battle":
|
||||
chain_battles += 1
|
||||
free.append(n)
|
||||
free = _shuffle(rg, free)
|
||||
var want := int(round(float(_pool.size()) * (float(_params["reward_ratio"]) + float(_params["rest_ratio"]))))
|
||||
var floor_n := int(_params["battle_floor"])
|
||||
var rest_first := rg.randf() < 0.5
|
||||
for i in range(want):
|
||||
if i >= free.size():
|
||||
break
|
||||
# 战斗保底优先:链上战斗数降到保底下限即停(侧链修型可能再让出战斗位,故保底只看链上)
|
||||
if chain_battles - 1 < floor_n:
|
||||
break
|
||||
var node = free[i]
|
||||
node["type"] = "rest" if ((i % 2 == 0) == rest_first) else "reward"
|
||||
chain_battles -= 1
|
||||
|
||||
|
||||
## 岔口修型段(零 RNG):侧链首节点类型取岔口各去路下一节点类型的补集首件——下一跳互异承面
|
||||
func _fix_detour_heads() -> void:
|
||||
for head in _detour_heads:
|
||||
var h = head
|
||||
var fork = null
|
||||
@@ -416,12 +499,15 @@ static func _trunk_len(L: int, e: int) -> int:
|
||||
|
||||
# ---- 自证辅助 ----
|
||||
|
||||
func _longest_paths(nodes: Array, by_id: Dictionary, entries: Array, indeg: Dictionary) -> Dictionary:
|
||||
func _longest_paths(nodes: Array, by_id: Dictionary, entries: Array) -> Dictionary:
|
||||
var out := {}
|
||||
var din := {}
|
||||
var q := []
|
||||
for id in indeg:
|
||||
din[int(id)] = int(indeg[int(id)])
|
||||
for n in nodes:
|
||||
din[int(n["id"])] = 0
|
||||
for n in nodes:
|
||||
for oid in (n["next"] as Array):
|
||||
din[int(oid)] = int(din[int(oid)]) + 1
|
||||
for eid in entries:
|
||||
out[int(eid)] = 0
|
||||
q.append(int(eid))
|
||||
@@ -476,44 +562,53 @@ func _check_fork_rules(nodes: Array, by_id: Dictionary, errs: Array) -> void:
|
||||
if head_types.has(ty):
|
||||
errs.append({"category": "structural", "message": "岔口 %d 各去路下一节点类型重复: %s" % [n["id"], ty]})
|
||||
head_types[ty] = true
|
||||
var reach_sets := []
|
||||
for oid in outs:
|
||||
reach_sets.append(_descendants(int(oid), by_id))
|
||||
var comps := []
|
||||
for i in range(reach_sets.size()):
|
||||
var excl := {}
|
||||
for rid in reach_sets[i]:
|
||||
var shared := false
|
||||
for j in range(reach_sets.size()):
|
||||
if j != i and reach_sets[j].has(rid):
|
||||
shared = true
|
||||
break
|
||||
if not shared:
|
||||
excl[int(rid)] = String(by_id[int(rid)]["type"])
|
||||
var arr := []
|
||||
for v in excl.values():
|
||||
arr.append(v)
|
||||
arr.sort()
|
||||
comps.append(arr)
|
||||
var comps := _fork_signatures(outs, by_id)
|
||||
for i in range(comps.size()):
|
||||
for j in range(i + 1, comps.size()):
|
||||
if comps[i] == comps[j]:
|
||||
errs.append({"category": "structural", "message": "岔口 %d 支路 %d/%d 类型构成相同" % [n["id"], i, j]})
|
||||
for i in range(comps.size()):
|
||||
var uniq := false
|
||||
for ty in comps[i]:
|
||||
var in_other := false
|
||||
for j in range(comps.size()):
|
||||
if j != i and (comps[j] as Array).has(ty):
|
||||
in_other = true
|
||||
break
|
||||
if not in_other:
|
||||
uniq = true
|
||||
break
|
||||
if not uniq:
|
||||
if not _fork_has_unique(comps, i):
|
||||
errs.append({"category": "structural", "message": "岔口 %d 支路 %d 无独有类型" % [n["id"], i]})
|
||||
|
||||
|
||||
## 支路签名构造:各去路的独占后代类型集(他人不可达的后代类型,排序数组——支路互斥段签名)
|
||||
func _fork_signatures(outs: Array, by_id: Dictionary) -> Array:
|
||||
var reach_sets := []
|
||||
for oid in outs:
|
||||
reach_sets.append(_descendants(int(oid), by_id))
|
||||
var comps := []
|
||||
for i in range(reach_sets.size()):
|
||||
var excl := {}
|
||||
for rid in reach_sets[i]:
|
||||
var shared := false
|
||||
for j in range(reach_sets.size()):
|
||||
if j != i and reach_sets[j].has(rid):
|
||||
shared = true
|
||||
break
|
||||
if not shared:
|
||||
excl[int(rid)] = String(by_id[int(rid)]["type"])
|
||||
var arr := []
|
||||
for v in excl.values():
|
||||
arr.append(v)
|
||||
arr.sort()
|
||||
comps.append(arr)
|
||||
return comps
|
||||
|
||||
|
||||
## 独有类型判定:支路 i 的签名中存在他支路不含的类型
|
||||
func _fork_has_unique(comps: Array, i: int) -> bool:
|
||||
for ty in comps[i]:
|
||||
var in_other := false
|
||||
for j in range(comps.size()):
|
||||
if j != i and (comps[j] as Array).has(ty):
|
||||
in_other = true
|
||||
break
|
||||
if not in_other:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _descendants(start: int, by_id: Dictionary) -> Dictionary:
|
||||
var out := {start: true}
|
||||
var q := [start]
|
||||
|
||||
@@ -81,8 +81,8 @@ func reset_paid() -> void:
|
||||
|
||||
# 本次 GP 付费刷新价(与商店货架刷新同源:ceil(基价 × 曲线^本次停点已付费刷))
|
||||
func gp_cost() -> int:
|
||||
return int(ceil(float(db.adventure.shop_refresh_cost_base)
|
||||
* pow(db.adventure.shop_refresh_cost_curve, float(_paid_refreshes))))
|
||||
return int(ceil(float(db.run.adventure.shop_refresh_cost_base)
|
||||
* pow(db.run.adventure.shop_refresh_cost_curve, float(_paid_refreshes))))
|
||||
|
||||
|
||||
# 冒险级复位(宿主 reset_adventure_state 调用——刷新三源与付费计数全零)
|
||||
|
||||
+40
-24
@@ -40,18 +40,8 @@ func roll_board(eligible: Array) -> bool:
|
||||
if eligible.is_empty():
|
||||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
|
||||
return false
|
||||
# 行主序规范化 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
|
||||
var seen := {}
|
||||
var list: Array[Dictionary] = []
|
||||
for e in eligible:
|
||||
var c: Vector2i = e.cell
|
||||
if seen.has(c):
|
||||
push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c))
|
||||
continue
|
||||
seen[c] = true
|
||||
list.append({"cell": c})
|
||||
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
|
||||
_eligible = list
|
||||
_eligible = _normalize_eligible(eligible)
|
||||
var list: Array[Dictionary] = _eligible
|
||||
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
|
||||
_power_roll.clear()
|
||||
_clear_power = -1
|
||||
@@ -85,6 +75,21 @@ func roll_board(eligible: Array) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
|
||||
func _normalize_eligible(eligible: Array) -> Array[Dictionary]:
|
||||
var seen := {}
|
||||
var list: Array[Dictionary] = []
|
||||
for e in eligible:
|
||||
var c: Vector2i = e.cell
|
||||
if seen.has(c):
|
||||
push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c))
|
||||
continue
|
||||
seen[c] = true
|
||||
list.append({"cell": c})
|
||||
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
|
||||
return list
|
||||
|
||||
|
||||
# ---- 查询面(Ready 只读;未开格恒可查——秘宝 桥接结构前提)----
|
||||
|
||||
# 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段)
|
||||
@@ -167,7 +172,7 @@ func retire_mapping() -> bool:
|
||||
# ---- 极性路由字典面(战斗结算 授予段消费):(类型, 极性)→ 格面池字典行(拷贝出口)----
|
||||
|
||||
func polarity_row(cell_type: String, polarity: String) -> Dictionary:
|
||||
for row in db.reward.rows:
|
||||
for row in db.run.reward.rows:
|
||||
if String(row.get("pool_tag", "")) == "cell" \
|
||||
and String(row.get("cell_type", "")) == cell_type \
|
||||
and String(row.get("polarity", "")) == polarity:
|
||||
@@ -217,6 +222,20 @@ func _skewed_cell_rows() -> Array:
|
||||
if board_skew <= 0:
|
||||
return _rollable_rows()
|
||||
var base: Array[Variant] = _rollable_rows()
|
||||
var families := _skew_families(base)
|
||||
if families["down"].is_empty() or families["up"].is_empty():
|
||||
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
|
||||
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
|
||||
var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200)
|
||||
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
|
||||
var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops))
|
||||
var out := _apply_skew(base, families["down"], families["up"], drops, gains)
|
||||
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
|
||||
return out
|
||||
|
||||
|
||||
## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰)
|
||||
func _skew_families(base: Array) -> Dictionary:
|
||||
var down: Array[Variant] = []
|
||||
var up: Array[Variant] = []
|
||||
for row in base:
|
||||
@@ -225,13 +244,11 @@ func _skewed_cell_rows() -> Array:
|
||||
down.append(row)
|
||||
elif SKEW_UP_KINDS.has(k):
|
||||
up.append(row)
|
||||
if down.is_empty() or up.is_empty():
|
||||
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
|
||||
var v := board_skew
|
||||
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
|
||||
var drops := _largest_remainder_split(down, v, 100, (2 * _pool_weight(down) * v + 100) / 200)
|
||||
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
|
||||
var gains := _largest_remainder_split(up, drops_total(drops), _pool_weight(up), drops_total(drops))
|
||||
return {"down": down, "up": up}
|
||||
|
||||
|
||||
## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移)
|
||||
func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array:
|
||||
var di := 0
|
||||
var ui := 0
|
||||
var out: Array[Variant] = []
|
||||
@@ -248,7 +265,6 @@ func _skewed_cell_rows() -> Array:
|
||||
var skewed: Dictionary[Variant, Variant] = row.duplicate()
|
||||
skewed["weight"] = w
|
||||
out.append(skewed)
|
||||
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
|
||||
return out
|
||||
|
||||
|
||||
@@ -309,7 +325,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
func _rollable_rows() -> Array:
|
||||
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
|
||||
var out: Array[Dictionary] = []
|
||||
for row in db.reward.rows:
|
||||
for row in db.run.reward.rows:
|
||||
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
|
||||
var iid := String(row.get("effect_id", ""))
|
||||
if iid == "" or not _treasure_unlocked.has(iid):
|
||||
@@ -321,7 +337,7 @@ func _rollable_rows() -> Array:
|
||||
|
||||
func _pool_rows(pool_tag: String) -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for row in db.reward.rows:
|
||||
for row in db.run.reward.rows:
|
||||
if row.pool_tag == pool_tag and int(row.weight) > 0:
|
||||
out.append(row)
|
||||
return out
|
||||
@@ -335,7 +351,7 @@ func _pool_weight(rows: Array) -> int:
|
||||
|
||||
|
||||
func _row_by_id(rid: String) -> Dictionary:
|
||||
for row in db.reward.rows:
|
||||
for row in db.run.reward.rows:
|
||||
if row.reward_id == rid:
|
||||
return row
|
||||
return {}
|
||||
|
||||
@@ -120,7 +120,7 @@ func _charge_gate(v: Dictionary, target: Vector2i) -> Dictionary:
|
||||
for h in tu_hits:
|
||||
if String(h.get("kind", "")) == "counter_spend":
|
||||
if not EffectEngine.counter_spend(fx_host.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 1))):
|
||||
var vrow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.item_id))
|
||||
var vrow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(v.item_id))
|
||||
var cn_info: int = int(vrow.get("charge_n", 0)) # 读受理行自身
|
||||
var seen := int(attune_safe_seen_cb.call())
|
||||
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), seen % cn_info if cn_info > 0 else 0, cn_info]}
|
||||
@@ -199,8 +199,8 @@ func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool:
|
||||
# premium_pct 贯穿 _apply_percent_row 应用与呈现同乘)——标空/挖格/清图不乘,雷安同池冻结不动。
|
||||
# 取整=四舍五入(与伤害管线 round_direction=floor 分域——registry clause3_inequality 登记;
|
||||
# 1 点量纲行 ac_up/action 取整后不变=整数粒度地板)。
|
||||
if source_class == "seal_hit" and int(db.settlement.core.seal_hit_premium) != 100:
|
||||
var prem_pct: int = int(db.settlement.core.seal_hit_premium)
|
||||
if source_class == "seal_hit" and int(db.combat.settlement.core.seal_hit_premium) != 100:
|
||||
var prem_pct: int = int(db.combat.settlement.core.seal_hit_premium)
|
||||
row = row.duplicate()
|
||||
row["power"] = int(round(int(row.power) * prem_pct / 100.0))
|
||||
row["premium_pct"] = prem_pct
|
||||
@@ -229,7 +229,7 @@ func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool:
|
||||
var eff := String(row.effect_id)
|
||||
if eff == "gp_gain" or eff == "gp_loss":
|
||||
return _apply_gp_row(row, att)
|
||||
var srow: Dictionary[Variant, Variant] = db.status_effect_row(eff)
|
||||
var srow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff)
|
||||
# 封印溢价贯穿(仅封印命中正向行携带 premium_pct;其余授予路径缺省 100 零扰动):
|
||||
# 应用幅度与呈现行值同乘,容器经 magnitude_override 拿同一快照(抗表重载口径保持)。
|
||||
var prem_pct: int = int(row.get("premium_pct", 100))
|
||||
|
||||
+143
-519
@@ -4,6 +4,8 @@
|
||||
## 禁 preload/import 任何玩法脚本、禁碰 ConfigDB/RngService——须可独立实例化独立验证。
|
||||
## 新增存档键先在序列化函数登记并升版本号;键域内容口径归各 GDD 文档,本类照单承载。
|
||||
## 存档键清单的事实源=本类序列化/读回代码,不在任何文档维护第二份键清单。
|
||||
## 拆件形态(D3 存档域):槽位文件域归 SlotStore(slots)、存档内元进度数据读写归 MetaStateStore
|
||||
## (meta_state)——本类留磁盘 IO+版本迁移链+断点域+门面装配,消费方经 save.slots / save.meta_state 直连。
|
||||
class_name SaveManagerCore
|
||||
extends Node
|
||||
|
||||
@@ -18,18 +20,18 @@ const SLOT_PATH_TEMPLATE := "user://save_slot_%d.json" # 槽文件名唯一派
|
||||
const LEGACY_SAVE_PATH := "user://save.json" # 旧隐式单档路径(Q1 甲:首次启动一次性字节迁入槽 1,此后无任何隐式读写)
|
||||
const LEGACY_MIGRATED_SUFFIX := ".migrated.bak" # 迁移后旧档改名留痕后缀(字节级留盘不删——处置留痕)
|
||||
|
||||
var _path: String # 当前写盘目标=激活槽文件(或 attach_path 注入路径);""=无激活态——一切写盘路径拒绝
|
||||
var _active_slot := 0 # 激活槽号 1..SLOT_COUNT;0=无激活态(注入非槽派生名路径时亦 0——写盘判定以 _path 非空为准)
|
||||
var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写)
|
||||
var _data: Dictionary[String, Variant] = {} # 内存数据模型(_init 即内置默认;load 后为档案内容)
|
||||
var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;v1→v6 生产迁移链现行——_init 自注册)
|
||||
var _slot_template := SLOT_PATH_TEMPLATE # 槽文件名模板(生产=常量;测试经 attach_slot_template 注入隔离命名空间,禁在呈现层/探针各自拼槽名)
|
||||
var slots: SlotStore # 槽位文件域件(槽位挂载与模板/激活与建删/摘要窥视——架构 :89)
|
||||
var meta_state: MetaStateStore # 存档内元进度数据读写唯一层(写口原语+只读投影——架构 :90)
|
||||
var _migration_fail_reason := "" # 迁移循环失败原因带出(_peek_migrated 返 null 时 load 侧落 corrupt 兜底文案单点)
|
||||
|
||||
|
||||
func _init(path: String = "") -> void:
|
||||
_path = path
|
||||
_active_slot = _slot_index_of(path)
|
||||
_data = _default_data()
|
||||
slots = SlotStore.new(self, path)
|
||||
meta_state = MetaStateStore.new(self)
|
||||
# 生产迁移链自注册(v2 起——测试钩 register_migration 仍可叠挂更早版本步骤)
|
||||
register_migration(1, _migrate_v1_v2)
|
||||
register_migration(2, _migrate_v2_v3)
|
||||
@@ -43,176 +45,103 @@ func _init(path: String = "") -> void:
|
||||
# 类名加 Core 后缀、autoload 占净名)。独立 .new() 实例(单测/工具)不入树零 _ready,
|
||||
# 读盘仍由调用方显式触发。
|
||||
# 启动态钉死(卡① sv2):autoload 启动不读任何槽——内存=默认合法空态(_init 置好)、
|
||||
# 无激活态(_path="",一切写盘路径拒绝并返回 false);玩家侧经选档界面显式进档/新档后才可写。
|
||||
# 无激活态(注入路径空,一切写盘路径拒绝并返回 false);玩家侧经选档界面显式进档/新档后才可写。
|
||||
# 旧隐式单档一次性迁入槽 1(Q1 甲,sv12)在本口执行。
|
||||
func _ready() -> void:
|
||||
_migrate_legacy_save(LEGACY_SAVE_PATH, slot_path(1), LEGACY_SAVE_PATH + LEGACY_MIGRATED_SUFFIX)
|
||||
slots._migrate_legacy_save(LEGACY_SAVE_PATH, slots.slot_path(1), LEGACY_SAVE_PATH + LEGACY_MIGRATED_SUFFIX)
|
||||
|
||||
|
||||
# 原地重置(探针隔离面,仿 RngService.set_master_seed 单例原地重置:
|
||||
# 换路径+回默认数据+重读盘;对象引用恒定,MetaProgression 持有的 save 引用零失效)。
|
||||
# 三槽化后单路径语义(契约 7)=注入路径即激活槽文件(写盘落该路径、无激活态时注入即激活);
|
||||
# 注入槽派生名时激活号同步记账,注入任意隔离路径时激活号 0(写盘仍合法——隔离面行为零变化)。
|
||||
func attach_path(path: String) -> void:
|
||||
_path = path
|
||||
# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约:
|
||||
# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v6,_init 自注册),
|
||||
# 冒烟经本口验证链式升级。
|
||||
func register_migration(from_version: int, fn: Callable) -> void:
|
||||
_migrations[from_version] = fn
|
||||
|
||||
|
||||
# ---- 读盘(进档/注入时调用;启动态不读——_ready 已钉死不隐式激活)----
|
||||
# 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。
|
||||
# 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。
|
||||
func load_from_disk() -> bool:
|
||||
_read_only = false
|
||||
_active_slot = _slot_index_of(path)
|
||||
_data = _default_data()
|
||||
load_from_disk()
|
||||
|
||||
|
||||
# 测试隔离注入口(契约 7 同源——槽文件名模板整体换注,探针/单测经此隔离到自命名空间,
|
||||
# 禁各自拼槽文件名;换注后回无激活态,槽原语全部落注入命名空间)
|
||||
func attach_slot_template(template: String) -> void:
|
||||
_slot_template = template
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_read_only = false
|
||||
_data = _default_data()
|
||||
|
||||
|
||||
# ---- 三存档位承载(卡①:隐式单档 → 3 存档位;位数与文件名口径=SLOT_COUNT/SLOT_PATH_TEMPLATE)----
|
||||
|
||||
# 槽文件路径唯一派生口(模板单点——禁第二处拼名)
|
||||
func slot_path(slot: int) -> String:
|
||||
return _slot_template % slot
|
||||
|
||||
|
||||
# 激活槽号查询(0 = 无激活态——启动态钉死后 boot 需经选档显式进档/新档,卡③ Q1 甲)
|
||||
func active_slot() -> int:
|
||||
return _active_slot
|
||||
|
||||
|
||||
# 写盘目标在位与否(激活槽或注入路径任一;无激活态一切写盘路径拒绝——sv3 判定口)
|
||||
func has_write_target() -> bool:
|
||||
return _path != ""
|
||||
|
||||
|
||||
# 进档原语(sv5,契约 6/8):激活槽 N+读盘,复用 load_from_disk 迁移/兜底链按槽成立。
|
||||
# 高版本拒读槽:激活不成立、恢复切换前三态(路径/激活号/禁写位/数据)——防误写他槽;
|
||||
# 损坏槽:兜底链接管(.corrupt.bak 改名+空态继续,激活成立)。
|
||||
func activate_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro := _read_only
|
||||
var prev_data := _data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_read_only = false
|
||||
_data = _default_data()
|
||||
if not load_from_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_read_only = prev_ro
|
||||
_data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 新档原语(sv4):空槽创建=合法白板态(_default_data 全零可玩纪律),创建即激活并落盘。
|
||||
# 生产路径只在空槽调用(有档槽开新局=先删档回空再新档,卡③ 契约 4);写盘失败回退切换前三态。
|
||||
func create_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro := _read_only
|
||||
var prev_data := _data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_read_only = false
|
||||
_data = _default_data()
|
||||
if not write_to_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_read_only = prev_ro
|
||||
_data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 删档原语(sv6,契约 8):该槽文件删除+盘面即时生效(卡内工程规定,挪用现役断点覆盖流语义)
|
||||
# +无 .tmp 残留;删激活槽时激活指针连清除(内存回默认空态,该槽不再可写——激活态生命周期终点)。
|
||||
# .corrupt.bak 损坏备份不追删(用户数据留盘可考;同槽再损坏时兜底流按槽覆盖同名备份)。
|
||||
# 删不存在文件的空槽=幂等成功;越界槽号拒绝。
|
||||
func delete_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return false
|
||||
var p := slot_path(slot)
|
||||
if FileAccess.file_exists(p) and not _remove_file(p):
|
||||
push_error("[存档序列化] 删档失败(文件删除被拒):%s" % p)
|
||||
return false
|
||||
var tmp := p + ".tmp"
|
||||
if FileAccess.file_exists(tmp):
|
||||
_remove_file(tmp) # 残留清偿尽力而为(主文件已删即达成空槽语义)
|
||||
if _active_slot == slot:
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_data = _default_data()
|
||||
return true
|
||||
|
||||
|
||||
# 摘要三项按槽窥视读器(sv7/sv8,契约 5/9):钥石余额/冒险成功局数/冒险总局数。
|
||||
# 只读零副作用:不激活、不写盘、不触发 _corrupt_fallback 改名(.corrupt.bak 只在正式进档路径产生)。
|
||||
# 返回 {"readable", "has_file", "keystone", "cleared", "total"}——readable=true=可读槽(空槽=零值合法态,
|
||||
# .tmp 残留按空槽——主文件不在即空);readable=false=不可读槽(损坏/高版本/缺版本字段/打开失败,
|
||||
# 三值无意义——卡③ Q2 裁甲:UI 按空槽呈现,进档时走正式 load 兜底/拒读链);
|
||||
# has_file=主文件在盘与否(空/有档操作集二分判定来处——卡③ T0 对齐结论;非摘要第四项,零 UI 语义)。
|
||||
# 迁移链在此只读推演(生产迁移 Callable 为纯函数,零盘面副作用)。
|
||||
func peek_slot_summary(slot: int) -> Dictionary:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽
|
||||
return _peek_summary_at(slot_path(slot))
|
||||
|
||||
|
||||
# 窥视注入口(契约 7 隔离面同款——测试/探针经路径注入隔离,禁各自拼槽文件名)
|
||||
func peek_summary_at(path: String) -> Dictionary:
|
||||
return _peek_summary_at(path)
|
||||
|
||||
|
||||
func _peek_summary_at(path: String) -> Dictionary:
|
||||
var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0}
|
||||
if not FileAccess.file_exists(path):
|
||||
out["readable"] = true # 空槽=可读零值合法态
|
||||
return out
|
||||
out["has_file"] = true # 主文件在盘(可读性与占档判定分离——白板档与空槽自此可区分)
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if not FileAccess.file_exists(slots._path):
|
||||
_data = _default_data() # 新档合法态
|
||||
return true
|
||||
var f := FileAccess.open(slots._path, FileAccess.READ)
|
||||
if f == null:
|
||||
return out
|
||||
return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error()))
|
||||
return _load_parsed(f)
|
||||
|
||||
|
||||
## 读盘解析域(load_from_disk 主体段提取——解析/版本/迁移/规整;corrupt_fallback 调用点随段迁移,副作用序逐字)
|
||||
func _load_parsed(f: FileAccess) -> bool:
|
||||
var text := f.get_as_text()
|
||||
f.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("version"):
|
||||
return out
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return _corrupt_fallback("JSON 解析失败或顶层结构出域(非对象)")
|
||||
if not parsed.has("version"):
|
||||
return _corrupt_fallback("缺版本字段")
|
||||
var v := int(parsed["version"])
|
||||
if v > SAVE_FORMAT_VERSION:
|
||||
return out
|
||||
while v < SAVE_FORMAT_VERSION:
|
||||
if not _migrations.has(v):
|
||||
return out
|
||||
var stepped = _migrations[v].call(parsed)
|
||||
if typeof(stepped) != TYPE_DICTIONARY:
|
||||
return out
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
var norm := _normalize(parsed)
|
||||
out["readable"] = true
|
||||
out["keystone"] = int(norm["meta"].get("keystone_balance", 0))
|
||||
out["cleared"] = int(norm["meta"].get("adventure_cleared", 0))
|
||||
out["total"] = int(norm["meta"].get("adventure_total", 0))
|
||||
return out
|
||||
_read_only = true
|
||||
_data = _default_data()
|
||||
push_error("[存档序列化] 档案版本 %d 高于当前 %d——拒读禁写(保护未来档不被覆写,内存默认新档运行)" % [v, SAVE_FORMAT_VERSION])
|
||||
return false
|
||||
var fail := []
|
||||
var migrated = _peek_migrated(parsed, fail)
|
||||
if migrated == null:
|
||||
return _corrupt_fallback(fail[0])
|
||||
_data = _normalize(migrated)
|
||||
# 老档迁移(关卡节点图生成 v0.4 布尔位——缺字段 = 已有游玩史,不重放教学脚本)
|
||||
if not _data["meta"].has("first_adventure_done"):
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
return true
|
||||
|
||||
|
||||
# 注入路径的槽号反查(命中槽派生名返回槽号,否则 0——激活号记账用,写盘判定以 _path 非空为准)
|
||||
func _slot_index_of(path: String) -> int:
|
||||
for i in range(1, SLOT_COUNT + 1):
|
||||
if path == slot_path(i):
|
||||
return i
|
||||
return 0
|
||||
# ---- 写盘(tmp + rename 原子写——写一半断电不留烂档)----
|
||||
# 无激活态一律拒绝并返回 false(sv3:启动态不隐式激活,盘面零触碰);
|
||||
# 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。
|
||||
func write_to_disk() -> bool:
|
||||
if slots._path == "":
|
||||
push_error("[存档序列化] 写盘拒绝——无激活存档位(启动态不隐式激活;经选档进档/新档后落盘)")
|
||||
return false
|
||||
if _read_only:
|
||||
push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)")
|
||||
return false
|
||||
var tmp := slots._path + ".tmp"
|
||||
var f := FileAccess.open(tmp, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())])
|
||||
return false
|
||||
f.store_string(JSON.stringify(_data, "\t", true))
|
||||
f.close()
|
||||
if not _rename_overwrite(tmp, slots._path):
|
||||
push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, slots._path])
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# ---- 冒险断点域(停点粒度续玩快照;内容装配归 AdventureRunner,本类照单承载)----
|
||||
# 空字典 = 无断点合法态。值拷贝进出(禁碰内部结构契约同款)——快照写入后调用方再改不渗入存档态。
|
||||
|
||||
func set_breakpoint(bp: Dictionary) -> void:
|
||||
_data["breakpoint"] = bp.duplicate(true)
|
||||
|
||||
|
||||
func clear_breakpoint() -> void:
|
||||
_data["breakpoint"] = {}
|
||||
|
||||
|
||||
func has_breakpoint() -> bool:
|
||||
return not (_data.get("breakpoint", {}) as Dictionary).is_empty()
|
||||
|
||||
|
||||
func breakpoint_snapshot() -> Dictionary:
|
||||
return (_data.get("breakpoint", {}) as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
# ---- 私有:迁移链 / 损坏兜底 / 规整 / 文件原语 ----
|
||||
|
||||
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
|
||||
func _migrate_v1_v2(data: Dictionary) -> Dictionary:
|
||||
if not data.has("breakpoint"):
|
||||
@@ -266,6 +195,53 @@ func _migrate_v6_v7(data: Dictionary) -> Dictionary:
|
||||
return data
|
||||
|
||||
|
||||
# 迁移循环提取(load/peek 两调用方同构共享):成功=迁移完成 Dictionary;失败=null
|
||||
# (原因经 fail 数组带出——load 侧落 corrupt 兜底文案、peek 侧保持「返回未填充 out」逐字)
|
||||
func _peek_migrated(parsed: Variant, fail: Array) -> Variant:
|
||||
var v := int(parsed["version"])
|
||||
while v < SAVE_FORMAT_VERSION:
|
||||
if not _migrations.has(v):
|
||||
fail.append("迁移链缺步骤 %d→%d" % [v, v + 1])
|
||||
return null
|
||||
var stepped = _migrations[v].call(parsed)
|
||||
if typeof(stepped) != TYPE_DICTIONARY:
|
||||
fail.append("迁移步骤 %d→%d 执行失败" % [v, v + 1])
|
||||
return null
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
return parsed
|
||||
|
||||
|
||||
## 摘要窥视内核(只读推演单点——SlotStore 两 peek 方法经槽路径派生委托至此;
|
||||
## 迁移链在此只读推演——生产迁移 Callable 为纯函数,零盘面副作用)
|
||||
func _peek_summary_at(path: String) -> Dictionary:
|
||||
var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0}
|
||||
if not FileAccess.file_exists(path):
|
||||
out["readable"] = true # 空槽=可读零值合法态
|
||||
return out
|
||||
out["has_file"] = true # 主文件在盘(可读性与占档判定分离——白板档与空槽自此可区分)
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
return out
|
||||
var text := f.get_as_text()
|
||||
f.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("version"):
|
||||
return out
|
||||
if int(parsed["version"]) > SAVE_FORMAT_VERSION:
|
||||
return out
|
||||
var fail := []
|
||||
var migrated = _peek_migrated(parsed, fail)
|
||||
if migrated == null:
|
||||
return out
|
||||
var norm := _normalize(migrated)
|
||||
out["readable"] = true
|
||||
out["keystone"] = int(norm["meta"].get("keystone_balance", 0))
|
||||
out["cleared"] = int(norm["meta"].get("adventure_cleared", 0))
|
||||
out["total"] = int(norm["meta"].get("adventure_total", 0))
|
||||
return out
|
||||
|
||||
|
||||
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
|
||||
|
||||
func _default_data() -> Dictionary[String, Variant]:
|
||||
@@ -299,368 +275,15 @@ func _default_data() -> Dictionary[String, Variant]:
|
||||
}
|
||||
|
||||
|
||||
# ---- 首次冒险布尔位(关卡节点图生成 v1.1 确定性承载)----
|
||||
# false = 本档尚无游玩史 → 首局走固定种子 42 + 参数档生成图(教学与存档复现的固定图由此承载,无兜底模板);
|
||||
# 置位只在冒险收束挂点(MetaProgression.settle_adventure)。
|
||||
# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放首局固定图)。
|
||||
# 原地重置默认态(SlotStore 槽位原语的协作口——激活/建删/换注回默认数据)
|
||||
func _reset_to_default() -> void:
|
||||
_data = _default_data()
|
||||
|
||||
func is_first_adventure_done() -> bool:
|
||||
return bool(_data["meta"].get("first_adventure_done", true))
|
||||
|
||||
|
||||
func mark_first_adventure_done() -> void:
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
|
||||
|
||||
# ---- 读盘(进档/注入时调用;启动态不读——_ready 已钉死不隐式激活)----
|
||||
# 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。
|
||||
# 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。
|
||||
func load_from_disk() -> bool:
|
||||
_read_only = false
|
||||
if not FileAccess.file_exists(_path):
|
||||
_data = _default_data() # 新档合法态
|
||||
return true
|
||||
var f := FileAccess.open(_path, FileAccess.READ)
|
||||
if f == null:
|
||||
return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error()))
|
||||
var text := f.get_as_text()
|
||||
f.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return _corrupt_fallback("JSON 解析失败或顶层结构出域(非对象)")
|
||||
if not parsed.has("version"):
|
||||
return _corrupt_fallback("缺版本字段")
|
||||
var v := int(parsed["version"])
|
||||
if v > SAVE_FORMAT_VERSION:
|
||||
_read_only = true
|
||||
_data = _default_data()
|
||||
push_error("[存档序列化] 档案版本 %d 高于当前 %d——拒读禁写(保护未来档不被覆写,内存默认新档运行)" % [v, SAVE_FORMAT_VERSION])
|
||||
return false
|
||||
while v < SAVE_FORMAT_VERSION:
|
||||
if not _migrations.has(v):
|
||||
return _corrupt_fallback("迁移链缺步骤 %d→%d" % [v, v + 1])
|
||||
var stepped = _migrations[v].call(parsed)
|
||||
if typeof(stepped) != TYPE_DICTIONARY:
|
||||
return _corrupt_fallback("迁移步骤 %d→%d 执行失败" % [v, v + 1])
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
_data = _normalize(parsed)
|
||||
# 老档迁移(关卡节点图生成 v0.4 布尔位——缺字段 = 已有游玩史,不重放教学脚本)
|
||||
if not _data["meta"].has("first_adventure_done"):
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
return true
|
||||
|
||||
|
||||
# ---- 写盘(tmp + rename 原子写——写一半断电不留烂档)----
|
||||
# 无激活态一律拒绝并返回 false(sv3:启动态不隐式激活,盘面零触碰);
|
||||
# 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。
|
||||
func write_to_disk() -> bool:
|
||||
if _path == "":
|
||||
push_error("[存档序列化] 写盘拒绝——无激活存档位(启动态不隐式激活;经选档进档/新档后落盘)")
|
||||
return false
|
||||
if _read_only:
|
||||
push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)")
|
||||
return false
|
||||
var tmp := _path + ".tmp"
|
||||
var f := FileAccess.open(tmp, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())])
|
||||
return false
|
||||
f.store_string(JSON.stringify(_data, "\t", true))
|
||||
f.close()
|
||||
if not _rename_overwrite(tmp, _path):
|
||||
push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, _path])
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约:
|
||||
# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v6,_init 自注册),
|
||||
# 冒烟经本口验证链式升级。
|
||||
func register_migration(from_version: int, fn: Callable) -> void:
|
||||
_migrations[from_version] = fn
|
||||
|
||||
|
||||
# ---- 类型化域访问器(消费面——只经访问器操作,禁碰内部结构)----
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return int(_data["meta"]["keystone_balance"])
|
||||
|
||||
|
||||
func add_keystone(n: int) -> void:
|
||||
_data["meta"]["keystone_balance"] = keystone_balance() + n
|
||||
|
||||
|
||||
# 钥石消费唯一口:余额不足拒绝且零副作用(钥石只增不减——消费除外,元进度验收判据)
|
||||
func spend_keystone(n: int) -> bool:
|
||||
if keystone_balance() < n:
|
||||
return false
|
||||
_data["meta"]["keystone_balance"] = keystone_balance() - n
|
||||
return true
|
||||
|
||||
|
||||
func item_unlocked(item_id: String) -> bool:
|
||||
return _data["meta"]["item_unlocks"].has(item_id)
|
||||
|
||||
|
||||
func unlock_item(item_id: String) -> void:
|
||||
_data["meta"]["item_unlocks"][item_id] = true
|
||||
|
||||
|
||||
func clear_count(level_index: int) -> int:
|
||||
return int(_data["meta"]["level_clear_counts"].get(str(level_index), 0))
|
||||
|
||||
|
||||
func boss_first_kill_marked(level_index: int) -> bool:
|
||||
return _data["meta"]["boss_first_kill"].has(str(level_index))
|
||||
|
||||
|
||||
# ---- 同调位账本域(里程碑谓词表计数源 + 装配关系 + Y 升级态)----
|
||||
|
||||
# Boss 击杀总数(含重复——底稿裁决 4 计数源;收束入账 = apply_adventure_summary 逐关卡完成关次 +1,
|
||||
# Boss 恒唯一终点故 levels_cleared 即本次冒险 Boss 击杀数)
|
||||
func boss_kill_count() -> int:
|
||||
return int(_data["meta"].get("boss_kill_count", 0))
|
||||
|
||||
|
||||
# ---- 生涯终身计数域(跨局汇总页数据源;只读面经访问器,
|
||||
# 写入唯一经 apply_adventure_summary 收束口,禁第二写入点)----
|
||||
|
||||
# 冒险总局数(口径=开局即计:三结局 completed/failed/abandoned 同经收束恰计一次,含放弃与失败)
|
||||
func adventure_total() -> int:
|
||||
return int(_data["meta"].get("adventure_total", 0))
|
||||
|
||||
|
||||
# 冒险成功局数(口径=三关全通才计 1——判定字段 summary.cleared,adventure_runner「completed」
|
||||
# 唯一出口=末关的关卡完成)
|
||||
func adventure_cleared() -> int:
|
||||
return int(_data["meta"].get("adventure_cleared", 0))
|
||||
|
||||
|
||||
# 真视类工具累计使用次数(信标网络解锁行为条件;写入唯一经 add_vision_used
|
||||
# 冒险收束聚合入账,受理计数点=battle 层经冒险层聚合)
|
||||
func vision_used() -> int:
|
||||
return int(_data["meta"].get("vision_used", 0))
|
||||
|
||||
|
||||
func add_vision_used(n: int) -> void:
|
||||
_data["meta"]["vision_used"] = maxi(0, vision_used() + maxi(0, n))
|
||||
|
||||
|
||||
# ---- 天赋树域(MetaProgression 树段读写面)----
|
||||
|
||||
func tree_nodes() -> Dictionary:
|
||||
return (_data["meta"].get("tree_nodes", {}) as Dictionary).duplicate()
|
||||
|
||||
|
||||
func tree_level(node_id: String) -> int:
|
||||
return int((_data["meta"].get("tree_nodes", {}) as Dictionary).get(node_id, 0))
|
||||
|
||||
|
||||
func set_tree_level(node_id: String, level: int) -> void:
|
||||
var tn: Dictionary[Variant, Variant] = _data["meta"].get("tree_nodes", {}) as Dictionary
|
||||
tn[node_id] = maxi(0, level)
|
||||
_data["meta"]["tree_nodes"] = tn
|
||||
|
||||
|
||||
func fork_sides() -> Dictionary:
|
||||
return (_data["meta"].get("fork_sides", {}) as Dictionary).duplicate()
|
||||
|
||||
|
||||
func fork_side(group: String) -> String:
|
||||
return String((_data["meta"].get("fork_sides", {}) as Dictionary).get(group, ""))
|
||||
|
||||
|
||||
func set_fork_side(group: String, side: String) -> void:
|
||||
var fs: Dictionary[Variant, Variant] = _data["meta"].get("fork_sides", {}) as Dictionary
|
||||
fs[group] = side
|
||||
_data["meta"]["fork_sides"] = fs
|
||||
|
||||
|
||||
# ---- 锻造工坊域 forge_routes(MetaProgression 锻造写口读写面;无特效域)----
|
||||
# 解锁态不落档(派生):初始持有=表 initial_owned、练满前一档=levels[prereq]≥cap——判定单点在
|
||||
# MetaProgression.forge_route_unlocked(读表+读档);本类只承载直购清单 unlocked 与选中标记。
|
||||
# 域结构:{levels:{六路线 id→等级}, unlocked:[直购路线 id], weapon:当前武器路线 id, armor:当前甲路线 id}
|
||||
|
||||
func _forge_domain() -> Dictionary:
|
||||
return _data["meta"].get("forge_routes", {}) as Dictionary
|
||||
|
||||
|
||||
func forge_routes_snapshot() -> Dictionary:
|
||||
return _forge_domain().duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态)
|
||||
|
||||
|
||||
func forge_route_level(route_id: String) -> int:
|
||||
var lv: Dictionary[Variant, Variant] = _forge_domain().get("levels", {}) as Dictionary
|
||||
return int(lv.get(route_id, 0))
|
||||
|
||||
|
||||
func set_forge_route_level(route_id: String, level: int) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var lv: Dictionary[Variant, Variant] = dom.get("levels", {}) as Dictionary
|
||||
lv[route_id] = maxi(0, level)
|
||||
dom["levels"] = lv
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func forge_unlocked_purchases() -> Array:
|
||||
return (_forge_domain().get("unlocked", []) as Array).duplicate()
|
||||
|
||||
|
||||
func add_forge_unlock(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var un: Array[Variant] = dom.get("unlocked", []) as Array
|
||||
if not un.has(route_id):
|
||||
un.append(route_id)
|
||||
dom["unlocked"] = un
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func forge_weapon() -> String:
|
||||
return String(_forge_domain().get("weapon", "sword"))
|
||||
|
||||
|
||||
func set_forge_weapon(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["weapon"] = route_id
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func forge_armor() -> String:
|
||||
return String(_forge_domain().get("armor", "light"))
|
||||
|
||||
|
||||
func set_forge_armor(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["armor"] = route_id
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
# 装配关系(同调目录 item_id 有序数组——装配序即呈现序;跨冒险持久,冒险中途不可换装〔底稿 §8.5〕)。
|
||||
# 局外进度事实(元进度域同族),非契约 2 反面清单的「局内道具库存」——口径见 存档序列化.md 契约 2。
|
||||
func attunement_equipped() -> Array:
|
||||
return (_data["meta"].get("attunement_equipped", []) as Array).duplicate()
|
||||
|
||||
|
||||
func set_attunement_equipped(item_ids: Array) -> void:
|
||||
var out: Array[Variant] = []
|
||||
for v in item_ids:
|
||||
out.append(String(v))
|
||||
_data["meta"]["attunement_equipped"] = out
|
||||
|
||||
|
||||
# Y 刷新升级购买等级(0 = 未购 → 每冒险刷新次数 0;等级行值归营地升级行族)
|
||||
func attune_refresh_level() -> int:
|
||||
return int(_data["meta"].get("attune_refresh_level", 0))
|
||||
|
||||
|
||||
func set_attune_refresh_level(n: int) -> void:
|
||||
_data["meta"]["attune_refresh_level"] = maxi(0, n)
|
||||
|
||||
|
||||
# 同调位计数源组装(Attunement 服务取数口——clear_count = 逐关完成次数合计〔含重复关,底稿 §2.1 口径〕;
|
||||
# clear_count(level_index) 逐关访问器零改动复用)
|
||||
func recent_summaries() -> Array:
|
||||
return _data["meta"]["recent_summaries"].duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态)
|
||||
|
||||
|
||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||
# 钥石入账(summary.keystone_total——逐杀掉落已在冒险内计入,boss_drops 参数为
|
||||
# 摘要归档副本不重复入账)+ 逐关卡完成关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计
|
||||
# (同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附
|
||||
# keystone_drops 副本——复盘可归因)+ 生涯终身计数自增(总局数开局即计:
|
||||
# 三结局同经本口恰计一次;冒险成功局数按 summary.cleared 判定三关全通才计 1。终身计数写入
|
||||
# 单口=本函数,禁第二写入点)。
|
||||
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
|
||||
_data["meta"]["adventure_total"] = adventure_total() + 1
|
||||
if bool(summary.get("cleared", false)):
|
||||
_data["meta"]["adventure_cleared"] = adventure_cleared() + 1
|
||||
add_keystone(int(summary.get("keystone_total", 0)))
|
||||
var cleared := int(summary.get("levels_cleared", 0))
|
||||
for li in range(1, cleared + 1):
|
||||
var key := str(li)
|
||||
_data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
||||
_data["meta"]["boss_first_kill"][key] = true
|
||||
_data["meta"]["boss_kill_count"] = boss_kill_count() + cleared # Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复)
|
||||
var entry: Dictionary[Variant, Variant] = summary.duplicate(true)
|
||||
entry["boss_drops"] = boss_drops.duplicate(true)
|
||||
var arr: Array[Variant] = _data["meta"]["recent_summaries"]
|
||||
arr.push_front(entry)
|
||||
while arr.size() > SUMMARY_CAP:
|
||||
arr.pop_back()
|
||||
|
||||
|
||||
# ---- 冒险断点域(停点粒度续玩快照;内容装配归 AdventureRunner,本类照单承载)----
|
||||
# 空字典 = 无断点合法态。值拷贝进出(禁碰内部结构契约同款)——快照写入后调用方再改不渗入存档态。
|
||||
|
||||
func set_breakpoint(bp: Dictionary) -> void:
|
||||
_data["breakpoint"] = bp.duplicate(true)
|
||||
|
||||
|
||||
func clear_breakpoint() -> void:
|
||||
_data["breakpoint"] = {}
|
||||
|
||||
|
||||
func has_breakpoint() -> bool:
|
||||
return not (_data.get("breakpoint", {}) as Dictionary).is_empty()
|
||||
|
||||
|
||||
func breakpoint_snapshot() -> Dictionary:
|
||||
return (_data.get("breakpoint", {}) as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
# ---- 私有:损坏兜底与文件原语 ----
|
||||
|
||||
# 旧隐式单档一次性迁移(Q1 甲,sv12):src 字节级原样搬运到 dst(零解析零改造——损坏/高版本
|
||||
# 旧档字节原样落槽 1,读取时由正式进档路径的兜底/拒读链接管,不静默丢数据);
|
||||
# dst 已存在不重迁(幂等判据——判别锚④:去判据则重复迁移覆盖槽 1);迁移成功后 src 改名
|
||||
# bak 留痕(不删——处置字节留痕,防再次隐式读写)。任一步失败:源保留原地,下次启动重试。
|
||||
# tmp+rename 原子形态与 write_to_disk 同纪律。生产调用=src/dst/bak 为 旧单档/槽 1/留痕名。
|
||||
func _migrate_legacy_save(src: String, dst: String, bak: String) -> void:
|
||||
if not FileAccess.file_exists(src):
|
||||
return # 执行前置:旧档不存在=无操作
|
||||
if FileAccess.file_exists(dst):
|
||||
return # 幂等判据:槽 1 已存在不重迁
|
||||
var tmp := dst + ".tmp"
|
||||
if not _copy_file(src, tmp):
|
||||
push_warning("[存档序列化] 旧单档迁移失败(源保留,下次启动重试):%s → %s" % [src, tmp])
|
||||
return
|
||||
if not _rename_overwrite(tmp, dst):
|
||||
push_warning("[存档序列化] 旧单档迁移改名失败(tmp 保留待查,源保留重试):%s → %s" % [tmp, dst])
|
||||
return
|
||||
_rename_overwrite(src, bak)
|
||||
push_warning("[存档序列化] 旧单档已迁入 %s,原件留痕于 %s" % [dst, bak])
|
||||
|
||||
|
||||
# 字节级文件拷贝(迁移专用——buffer 直拷,零解析零改造)
|
||||
func _copy_file(from: String, to: String) -> bool:
|
||||
var src := FileAccess.open(from, FileAccess.READ)
|
||||
if src == null:
|
||||
return false
|
||||
var dst := FileAccess.open(to, FileAccess.WRITE)
|
||||
if dst == null:
|
||||
src.close()
|
||||
return false
|
||||
while not src.eof_reached():
|
||||
var chunk := src.get_buffer(65536)
|
||||
if chunk.size() > 0:
|
||||
dst.store_buffer(chunk)
|
||||
src.close()
|
||||
dst.close()
|
||||
return true
|
||||
|
||||
|
||||
# 文件删除(删档原语专用——呈现层禁 DirAccess 直删,承载面单点)
|
||||
func _remove_file(p: String) -> bool:
|
||||
var dir := DirAccess.open(p.get_base_dir())
|
||||
if dir == null:
|
||||
return false
|
||||
return dir.remove(p.get_file()) == OK
|
||||
|
||||
# 损坏兜底:坏档改名 .corrupt.bak(已存在则覆盖)→ 默认数据继续。不静默丢档(警告留痕)、不 crash。
|
||||
func _corrupt_fallback(reason: String) -> bool:
|
||||
var bak := _path + ".corrupt.bak"
|
||||
if not _rename_overwrite(_path, bak):
|
||||
var bak := slots._path + ".corrupt.bak"
|
||||
if not _rename_overwrite(slots._path, bak):
|
||||
push_warning("[存档序列化] 坏档备份改名失败(原文件保留原地):%s——%s" % [bak, reason])
|
||||
else:
|
||||
push_warning("[存档序列化] 检测到损坏档案,已备份为 %s——%s(用默认新档继续)" % [bak, reason])
|
||||
@@ -689,7 +312,8 @@ func _normalize(data: Dictionary) -> Dictionary[String, Variant]:
|
||||
return merged
|
||||
|
||||
|
||||
# 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义)
|
||||
# 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义;IO 原语单点——
|
||||
# SlotStore 迁移流经本件调用)
|
||||
func _rename_overwrite(from: String, to: String) -> bool:
|
||||
var dir := DirAccess.open(from.get_base_dir())
|
||||
if dir == null:
|
||||
|
||||
@@ -78,7 +78,7 @@ func _enemy_actions(ri: int) -> void:
|
||||
if int(oc._enemy_system.hero_unit().hp) <= 0:
|
||||
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
|
||||
var skill_id := oc._enemy_system.pending_action(u)
|
||||
var srow: Dictionary[Variant, Variant] = oc.db.skill_row(skill_id)
|
||||
var srow: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(skill_id)
|
||||
if srow.is_empty():
|
||||
oc._fatal("敌实例待发动作悬空: %s" % skill_id)
|
||||
continue
|
||||
@@ -97,7 +97,7 @@ func _enemy_actions(ri: int) -> void:
|
||||
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
|
||||
var rider := 0
|
||||
if srow.has("rider_mult"):
|
||||
rider = maxi(int(oc.db.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
|
||||
rider = maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
|
||||
var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action",
|
||||
"%s %s" % [u.display_name, String(srow.display_name)],
|
||||
{"channel": "combat", "base_value": baseline, "target_rule": DamageController.TARGET_HERO,
|
||||
|
||||
@@ -147,7 +147,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
|
||||
if fx_host._fx_bonus_delta.has(axis):
|
||||
fx_host._fx_bonus_delta[axis] += int(h.get("delta", 0))
|
||||
"blast_scaled":
|
||||
var blast_v := maxi(int(oc.db.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
|
||||
var blast_v := maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
|
||||
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发",
|
||||
{"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE,
|
||||
"ri": ri, "origin_cell": cell})
|
||||
@@ -163,7 +163,7 @@ func _terrain_batch(ri: int) -> void:
|
||||
for evt in oc._facts_reveals:
|
||||
var vids: Array[Variant] = evt.get("variant_ids", [])
|
||||
for vid in vids:
|
||||
var vrow: Dictionary[Variant, Variant] = oc.db.variation.row_by_id(String(vid))
|
||||
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid))
|
||||
if String(vrow.get("effect", "")) != "flat_damage":
|
||||
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
|
||||
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内
|
||||
|
||||
+16
-16
@@ -34,7 +34,7 @@ func bind(refs: Dictionary, hooks: Dictionary) -> void:
|
||||
# 商店覆盖层数据源(只读投影——现价/已购档数在此现算,呈现层禁复算定价公式)
|
||||
func shop_products() -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for prow in db.shop_products():
|
||||
for prow in db.run.shop.product_rows():
|
||||
out.append({
|
||||
"product_id": String(prow.get("product_id", "")),
|
||||
"display_name": String(prow.get("display_name", "")),
|
||||
@@ -49,7 +49,7 @@ func shop_products() -> Array:
|
||||
# 现价 = base_price + price_level_bonus(level_index) + price_increment × 该商品已购档数
|
||||
# (散折价:economy_spend 激活时商品标价 ×80%——见 _spend_discounted)
|
||||
func current_price(prow: Dictionary) -> int:
|
||||
return _spend_discounted(int(prow.get("base_price", 0)) + db.shop_price_level_bonus(_level_index()) \
|
||||
return _spend_discounted(int(prow.get("base_price", 0)) + db.run.shop.price_level_bonus_at(_level_index()) \
|
||||
+ int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", ""))))
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ func current_price(prow: Dictionary) -> int:
|
||||
# + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折)
|
||||
func _spend_discounted(price: int) -> int:
|
||||
if _rules().has("economy_spend"):
|
||||
return maxi(1, int(floor(float(price) * float(db.shop.spend_discount))))
|
||||
return maxi(1, int(floor(float(price) * float(db.run.shop.spend_discount))))
|
||||
return price
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ func purchased_tier(product_id: String) -> int:
|
||||
# 购买(未知商品/属性键出域/余额不足拒绝——返回 false 零副作用):
|
||||
# 出账 → 加算区 += flat_value → 购血同步当前生命(封顶新上限)→ 已购档数 +1
|
||||
func buy_product(product_id: String) -> bool:
|
||||
var prow: Dictionary[Variant, Variant] = db.shop_product_row(product_id)
|
||||
var prow: Dictionary[Variant, Variant] = db.run.shop.product_row(product_id)
|
||||
if prow.is_empty():
|
||||
push_error("[关卡节点地图] 商店房未知商品: %s" % product_id)
|
||||
return false
|
||||
@@ -133,7 +133,7 @@ func leave() -> void:
|
||||
func _draw_shelf() -> void:
|
||||
shelf = []
|
||||
var sellable: Array[Variant] = []
|
||||
for row in db.shop_pool_rows():
|
||||
for row in db.run.shop.pool_rows():
|
||||
var shelf_kind := String(row.get("kind_id", ""))
|
||||
if _pool_cap.is_valid() and not bool(_pool_cap.call(shelf_kind, "")):
|
||||
continue # cap 满剔除(货架候选视图剔除;牌库本体不动)
|
||||
@@ -184,14 +184,14 @@ func pool_stock(kind: String) -> int:
|
||||
# A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)。relic 卡(保底占位——shop 词表外)
|
||||
# 基价 provisional 15(表 knob relic_base_price——shop.tres 词表扩列归数值批)。
|
||||
func pool_price(kind: String) -> int:
|
||||
var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind)
|
||||
var row: Dictionary[Variant, Variant] = db.run.shop.pool_row(kind)
|
||||
if row.is_empty():
|
||||
if kind == "relic":
|
||||
return _spend_discounted(int(ceil(float(db.shop.relic_base_price) * pow(db.shop_price_b(), float(maxi(0, _level_index() - 1))) * pow(db.shop_price_a(), float(visit_buys)))))
|
||||
return _spend_discounted(int(ceil(float(db.run.shop.relic_base_price) * pow(db.run.shop.price_b, float(maxi(0, _level_index() - 1))) * pow(db.run.shop.price_a, float(visit_buys)))))
|
||||
return 999999
|
||||
var base := float(maxi(1, int(row.get("base_price", 1))))
|
||||
var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, _level_index() - 1))) \
|
||||
* pow(db.shop_price_a(), float(visit_buys))
|
||||
var price_f: float = base * pow(db.run.shop.price_b, float(maxi(0, _level_index() - 1))) \
|
||||
* pow(db.run.shop.price_a, float(visit_buys))
|
||||
return _spend_discounted(int(ceil(price_f)))
|
||||
|
||||
|
||||
@@ -202,11 +202,11 @@ func shop_shelf() -> Array:
|
||||
for tok in shelf:
|
||||
var kind := String((tok as Dictionary).get("kind", ""))
|
||||
var tier := String((tok as Dictionary).get("tier", ""))
|
||||
var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind)
|
||||
var row: Dictionary[Variant, Variant] = db.run.shop.pool_row(kind)
|
||||
var display := String(row.get("display_name", kind))
|
||||
var value := int(db.adventure.curio_value(kind, tier))
|
||||
var value := int(db.run.adventure.curio_value(kind, tier))
|
||||
if row.is_empty() and kind == "relic":
|
||||
var rrow: Dictionary[Variant, Variant] = db.feat_row(String(tok.get("feat_id", "")))
|
||||
var rrow: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(String(tok.get("feat_id", "")))
|
||||
display = String(rrow.get("display_name", "特性"))
|
||||
value = 0
|
||||
out.append({
|
||||
@@ -227,22 +227,22 @@ func shop_shelf() -> Array:
|
||||
func shop_refresh_free_left() -> int:
|
||||
# 刷新执照 shop_refresh_bonus 装配合计叠加(进店入账语义=读数即本店可用,
|
||||
# 与基础 knob 同生命周期;未装配=基值)
|
||||
return maxi(0, int(db.adventure.shop_refresh_free) + _attune_shop_refresh_bonus())
|
||||
return maxi(0, int(db.run.adventure.shop_refresh_free) + _attune_shop_refresh_bonus())
|
||||
|
||||
|
||||
# 刷新执照装配合计(装配集 kind 分发扫描;零装配=0)
|
||||
func _attune_shop_refresh_bonus() -> int:
|
||||
var total := 0
|
||||
for iid_v in _attunement():
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||||
if (arow.get("effect_kinds", []) as Array).has("shop_refresh_bonus"):
|
||||
total += int(arow.get("magnitude", 0))
|
||||
return total
|
||||
|
||||
|
||||
func shop_refresh_cost() -> int:
|
||||
return int(ceil(float(db.adventure.shop_refresh_cost_base)
|
||||
* pow(db.adventure.shop_refresh_cost_curve, float(paid_refreshes))))
|
||||
return int(ceil(float(db.run.adventure.shop_refresh_cost_base)
|
||||
* pow(db.run.adventure.shop_refresh_cost_curve, float(paid_refreshes))))
|
||||
|
||||
|
||||
func shop_refresh() -> bool:
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
## 存档槽位文件域件(D3 拆件——自 save_manager 收编槽位族):槽位挂载与模板、激活与建删、
|
||||
## 摘要窥视(peek 族)的唯一所有者。槽位态(路径/激活号/模板)随件承载;数据重置与读写盘
|
||||
## 经注入的 save 内部协作家族(_reset_to_default/_load_parsed/_rename_overwrite)完成——
|
||||
## 本件零 _data 副本零缓存(load 整体换新不陈旧)。
|
||||
class_name SlotStore
|
||||
extends RefCounted
|
||||
|
||||
var _save # SaveManagerCore 注入引用(IO 与数据域协作单点)
|
||||
var _path: String = "" # 当前写盘目标=激活槽文件(或 attach_path 注入路径);""=无激活态——一切写盘路径拒绝
|
||||
var _active_slot := 0 # 激活槽号 1..SLOT_COUNT;0=无激活态(注入非槽派生名路径时亦 0——写盘判定以 _path 非空为准)
|
||||
var _slot_template: String # 槽文件名模板(生产=常量;测试经 attach_slot_template 注入隔离命名空间,禁在呈现层/探针各自拼槽名)
|
||||
|
||||
|
||||
func _init(save_ref, path: String = "") -> void:
|
||||
_save = save_ref
|
||||
_slot_template = _save.SLOT_PATH_TEMPLATE
|
||||
_path = path
|
||||
_active_slot = _slot_index_of(path)
|
||||
|
||||
|
||||
## 进档注入口:换路径+回默认数据+重读盘(对象引用恒定,MetaProgression 持有的 save 引用零失效)。
|
||||
## 三槽化后单路径语义(契约 7)=注入路径即激活槽文件(写盘落该路径、无激活态时注入即激活);
|
||||
## 注入槽派生名时激活号同步记账,注入任意隔离路径时激活号 0(写盘仍合法——隔离面行为零变化)。
|
||||
func attach_path(path: String) -> void:
|
||||
_path = path
|
||||
_save._read_only = false
|
||||
_active_slot = _slot_index_of(path)
|
||||
_save._reset_to_default()
|
||||
_save.load_from_disk()
|
||||
|
||||
|
||||
## 测试隔离注入口(契约 7 同源——槽文件名模板整体换注,探针/单测经此隔离到自命名空间,
|
||||
## 禁各自拼槽文件名;换注后回无激活态,槽原语全部落注入命名空间)
|
||||
func attach_slot_template(template: String) -> void:
|
||||
_slot_template = template
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_save._read_only = false
|
||||
_save._reset_to_default()
|
||||
|
||||
|
||||
# 槽文件路径唯一派生口(模板单点——禁第二处拼名)
|
||||
func slot_path(slot: int) -> String:
|
||||
return _slot_template % slot
|
||||
|
||||
|
||||
# 激活槽号查询(0 = 无激活态——启动态钉死后 boot 需经选档显式进档/新档)
|
||||
func active_slot() -> int:
|
||||
return _active_slot
|
||||
|
||||
|
||||
# 写盘目标在位与否(激活槽或注入路径任一;无激活态一切写盘路径拒绝——sv3 判定口)
|
||||
func has_write_target() -> bool:
|
||||
return _path != ""
|
||||
|
||||
|
||||
# 进档原语(sv5,契约 6/8):激活槽 N+读盘,复用 load_from_disk 迁移/兜底链按槽成立。
|
||||
# 高版本拒读槽:激活不成立、恢复切换前三态(路径/激活号/禁写位/数据)——防误写他槽;
|
||||
# 损坏槽:兜底链接管(.corrupt.bak 改名+空态继续,激活成立)。
|
||||
func activate_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro: bool = _save._read_only
|
||||
var prev_data = _save._data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_save._read_only = false
|
||||
_save._reset_to_default()
|
||||
if not _save.load_from_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_save._read_only = prev_ro
|
||||
_save._data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 新档原语(sv4):空槽创建=合法白板态(_default_data 全零可玩纪律),创建即激活并落盘。
|
||||
# 生产路径只在空槽调用(有档槽开新局=先删档回空再新档);写盘失败回退切换前三态。
|
||||
func create_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro: bool = _save._read_only
|
||||
var prev_data = _save._data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_save._read_only = false
|
||||
_save._reset_to_default()
|
||||
if not _save.write_to_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_save._read_only = prev_ro
|
||||
_save._data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 删档原语(sv6,契约 8):该槽文件删除+盘面即时生效+无 .tmp 残留;删激活槽时激活指针连清除
|
||||
# (内存回默认空态,该槽不再可写——激活态生命周期终点)。
|
||||
# .corrupt.bak 损坏备份不追删(用户数据留盘可考;同槽再损坏时兜底流按槽覆盖同名备份)。
|
||||
# 删不存在文件的空槽=幂等成功;越界槽号拒绝。
|
||||
func delete_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return false
|
||||
var p := slot_path(slot)
|
||||
if FileAccess.file_exists(p) and not _remove_file(p):
|
||||
push_error("[存档序列化] 删档失败(文件删除被拒):%s" % p)
|
||||
return false
|
||||
var tmp := p + ".tmp"
|
||||
if FileAccess.file_exists(tmp):
|
||||
_remove_file(tmp) # 残留清偿尽力而为(主文件已删即达成空槽语义)
|
||||
if _active_slot == slot:
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_save._reset_to_default()
|
||||
return true
|
||||
|
||||
|
||||
# 摘要三项按槽窥视读器(sv7/sv8,契约 5/9):钥石余额/冒险成功局数/冒险总局数。
|
||||
# 只读零副作用:不激活、不写盘、不触发 _corrupt_fallback 改名(.corrupt.bak 只在正式进档路径产生)。
|
||||
# 迁移链在此只读推演(内核 _peek_summary_at 归 save IO 域——本方法经槽路径派生委托)。
|
||||
func peek_slot_summary(slot: int) -> Dictionary:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽
|
||||
return _save._peek_summary_at(slot_path(slot))
|
||||
|
||||
|
||||
# 窥视注入口(契约 7 隔离面同款——测试/探针经路径注入隔离,禁各自拼槽文件名)
|
||||
func peek_summary_at(path: String) -> Dictionary:
|
||||
return _save._peek_summary_at(path)
|
||||
|
||||
|
||||
# 注入路径的槽号反查(命中槽派生名返回槽号,否则 0——激活号记账用,写盘判定以 _path 非空为准)
|
||||
func _slot_index_of(path: String) -> int:
|
||||
for i in range(1, _save.SLOT_COUNT + 1):
|
||||
if path == slot_path(i):
|
||||
return i
|
||||
return 0
|
||||
|
||||
|
||||
# 旧隐式单档一次性迁移(Q1 甲,sv12):src 字节级原样搬运到 dst(零解析零改造——损坏/高版本
|
||||
# 旧档字节原样落槽 1,读取时由正式进档路径的兜底/拒读链接管,不静默丢数据);
|
||||
# dst 已存在不重迁(幂等判据——判别锚④:去判据则重复迁移覆盖槽 1);迁移成功后 src 改名
|
||||
# bak 留痕(不删——处置字节留痕,防再次隐式读写)。任一步失败:源保留原地,下次启动重试。
|
||||
# tmp+rename 原子形态与 write_to_disk 同纪律(改名原语 _rename_overwrite 归 save IO 域单点)。生产调用=src/dst/bak 为 旧单档/槽 1/留痕名。
|
||||
func _migrate_legacy_save(src: String, dst: String, bak: String) -> void:
|
||||
if not FileAccess.file_exists(src):
|
||||
return # 执行前置:旧档不存在=无操作
|
||||
if FileAccess.file_exists(dst):
|
||||
return # 幂等判据:槽 1 已存在不重迁
|
||||
var tmp := dst + ".tmp"
|
||||
if not _copy_file(src, tmp):
|
||||
push_warning("[存档序列化] 旧单档迁移失败(源保留,下次启动重试):%s → %s" % [src, tmp])
|
||||
return
|
||||
if not _save._rename_overwrite(tmp, dst):
|
||||
push_warning("[存档序列化] 旧单档迁移改名失败(tmp 保留待查,源保留重试):%s → %s" % [tmp, dst])
|
||||
return
|
||||
_save._rename_overwrite(src, bak)
|
||||
push_warning("[存档序列化] 旧单档已迁入 %s,原件留痕于 %s" % [dst, bak])
|
||||
|
||||
|
||||
# 字节级文件拷贝(迁移专用——buffer 直拷,零解析零改造)
|
||||
func _copy_file(from: String, to: String) -> bool:
|
||||
var src := FileAccess.open(from, FileAccess.READ)
|
||||
if src == null:
|
||||
return false
|
||||
var dst := FileAccess.open(to, FileAccess.WRITE)
|
||||
if dst == null:
|
||||
src.close()
|
||||
return false
|
||||
while not src.eof_reached():
|
||||
var chunk := src.get_buffer(65536)
|
||||
if chunk.size() > 0:
|
||||
dst.store_buffer(chunk)
|
||||
src.close()
|
||||
dst.close()
|
||||
return true
|
||||
|
||||
|
||||
# 文件删除(删档原语专用——呈现层禁 DirAccess 直删,承载面单点)
|
||||
func _remove_file(p: String) -> bool:
|
||||
var dir := DirAccess.open(p.get_base_dir())
|
||||
if dir == null:
|
||||
return false
|
||||
return dir.remove(p.get_file()) == OK
|
||||
@@ -0,0 +1 @@
|
||||
uid://dylsba7whx5ql
|
||||
@@ -29,44 +29,58 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int
|
||||
if not _open:
|
||||
push_warning("[增益状态] 域闭授予(EC1)")
|
||||
return false
|
||||
var row: Dictionary[Variant, Variant] = db.status_effect_row(effect_id)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id)
|
||||
if row.is_empty():
|
||||
push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id)
|
||||
return false
|
||||
var policy: String = row.stack_policy
|
||||
var dur: int = int(row.duration_settlements) if duration_override < 1 else duration_override
|
||||
var max_stack: int = int(row.max_stack)
|
||||
# 授予时快照(抗表重载——EC9)
|
||||
var snap_m: int = int(row.magnitude)
|
||||
if source.has("magnitude_override"):
|
||||
snap_m = int(source["magnitude_override"]) # 封印溢价贯穿(迭代14② T4——封印命中 percent 形态同乘快照;缺省路径零扰动)
|
||||
var snap_unit: String = String(row.unit)
|
||||
var snap_display: String = String(row.display_name)
|
||||
var snap := _grant_snapshot(row, source)
|
||||
match policy:
|
||||
"independent":
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
"refresh":
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
inst.remaining = dur # R ← D(取实例快照 D——同表值);L 不变;空刷新恒 true
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
return _apply_refresh(effect_id, dur, snap, source)
|
||||
"stack":
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if int(inst.layers) >= max_stack:
|
||||
return false # 满层拒绝原子——容器零写入(拒收由战斗结算记"奖励未生效"行)
|
||||
inst.layers = int(inst.layers) + 1 # stack 授予不触碰 R(spec 决议)
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
return _apply_stack(effect_id, dur, snap, source, max_stack)
|
||||
push_warning("[增益状态] 未知 stack_policy")
|
||||
return false
|
||||
|
||||
|
||||
func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, m: int, unit: String, display: String) -> Dictionary:
|
||||
## 授予时快照域(抗表重载——EC9;封印溢价贯穿——迭代14② T4 percent 形态同乘快照、缺省路径零扰动)
|
||||
func _grant_snapshot(row: Dictionary, source: Dictionary) -> Dictionary:
|
||||
var snap_m: int = int(row.magnitude)
|
||||
if source.has("magnitude_override"):
|
||||
snap_m = int(source["magnitude_override"])
|
||||
return {"m": snap_m, "unit": String(row.unit), "display": String(row.display_name)}
|
||||
|
||||
|
||||
## refresh 臂:命中改 R 未命中追加(R ← D 取实例快照 D——同表值;L 不变;空刷新恒 true)
|
||||
func _apply_refresh(effect_id: String, dur: int, snap: Dictionary, source: Dictionary) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
inst.remaining = dur
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
|
||||
|
||||
## stack 臂:满层拒零写入·未满加层·未命中追加(stack 授予不触碰 R——spec 决议;
|
||||
## 满层拒绝原子——容器零写入,拒收由战斗结算记"奖励未生效"行)
|
||||
func _apply_stack(effect_id: String, dur: int, snap: Dictionary, source: Dictionary, max_stack: int) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if int(inst.layers) >= max_stack:
|
||||
return false
|
||||
inst.layers = int(inst.layers) + 1
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
|
||||
|
||||
func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, snap: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"effect_id": effect_id,
|
||||
"layers": layers,
|
||||
@@ -74,9 +88,9 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary,
|
||||
"source_class": String(source.get("source_class", "")),
|
||||
"origin_cell": source.get("origin_cell", Vector2i(-1, -1)),
|
||||
"round_index": int(source.get("round_index", 0)),
|
||||
"snap_m": m,
|
||||
"snap_unit": unit,
|
||||
"snap_display": display,
|
||||
"snap_m": snap["m"],
|
||||
"snap_unit": snap["unit"],
|
||||
"snap_display": snap["display"],
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +98,7 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary,
|
||||
func query_active(side: String) -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for inst in _instances:
|
||||
var row: Dictionary[Variant, Variant] = db.status_effect_row(inst.effect_id)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(inst.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
if String(row.target_side) != side:
|
||||
|
||||
+44
-26
@@ -68,9 +68,10 @@ func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool:
|
||||
func _item_row(item_id: String) -> Dictionary:
|
||||
if _db == null:
|
||||
return {}
|
||||
if _db.has_method("treasure_item_row"):
|
||||
return _db.treasure_item_row(item_id) # 表访问收口(正式面走 ConfigDB 访问器)
|
||||
return _db.treasure_items.row_by_id(item_id) # 桩表面(单测 StubDB 无访问器——直读保留,随测试面演进退役)
|
||||
var db_crossrun = _db.get("crossrun")
|
||||
if db_crossrun != null:
|
||||
return db_crossrun.treasure_items.row_by_id(item_id) # 表访问收口(正式面走 ConfigDB 容器直连)
|
||||
return _db.treasure_items.row_by_id(item_id) # 桩表面(单测 StubDB 无容器——直读保留,随测试面演进退役)
|
||||
|
||||
# 不标具体类型原因:单元测试注入 StubDB 桩表(treasure_items_test.gd)——typed 赋值会运行期拒收,保守保持 null 注
|
||||
var _db = null # ConfigDB(行定义读取——_init 注入;测试可注入桩表)
|
||||
@@ -97,31 +98,16 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre
|
||||
var kinds: Array[Variant] = row.get("effect_kinds", [])
|
||||
var resolved: Array[Variant] = []
|
||||
if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"):
|
||||
# 空间效果(真视 reveal_tiles/天眼复合 safe_dig 入空间解析;
|
||||
# 真视窄域护栏 REVEAL_KINDS 同守):
|
||||
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
|
||||
return _reject("目标越板")
|
||||
if String(row.get("target", "")) == "cell":
|
||||
# 单格档(线香真视单格;≤3×3 窄域契约内的点目标)
|
||||
resolved.append(target)
|
||||
else:
|
||||
# 3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
var c := target + Vector2i(dx, dy)
|
||||
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
|
||||
resolved.append(c)
|
||||
# 空间效果(真视 reveal_tiles/天眼复合 safe_dig 入空间解析;真视窄域护栏 REVEAL_KINDS 同守)
|
||||
var spatial := _spatial_reject_reason(row, target, board_size)
|
||||
if spatial != "":
|
||||
return _reject(spatial)
|
||||
resolved = _window_cells(row, target, board_size)
|
||||
# 点击级零信息:纯预读行窗口全已开 = 没东西可看(拒绝零消耗);
|
||||
# 真视恒可用(零雷窗口=「这 9 格没雷」信息非空——GDD 裁定)
|
||||
if kinds.has("reward_preview") and not kinds.has("mine_reveal"):
|
||||
var any_unrevealed := false
|
||||
if cell_unrevealed_cb.is_valid():
|
||||
for c in resolved:
|
||||
if bool(cell_unrevealed_cb.call(c)):
|
||||
any_unrevealed = true
|
||||
break
|
||||
if not any_unrevealed:
|
||||
return _reject("窗口内无可预读的未开格")
|
||||
if kinds.has("reward_preview") and not kinds.has("mine_reveal") \
|
||||
and _preview_window_dead(resolved, cell_unrevealed_cb):
|
||||
return _reject("窗口内无可预读的未开格")
|
||||
return {
|
||||
"ok": true,
|
||||
"reason": "",
|
||||
@@ -132,6 +118,38 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre
|
||||
}
|
||||
|
||||
|
||||
## 空间解析域:目标越板拒(cell/3×3 两档共用板界)——返回拒绝原因,空串=通过
|
||||
func _spatial_reject_reason(row: Dictionary, target: Vector2i, board_size: Vector2i) -> String:
|
||||
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
|
||||
return "目标越板"
|
||||
return ""
|
||||
|
||||
|
||||
## 预读窗口域:单格档=点目标(线香真视单格;≤3×3 窄域契约内);其余=3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
|
||||
func _window_cells(row: Dictionary, target: Vector2i, board_size: Vector2i) -> Array:
|
||||
var resolved: Array[Variant] = []
|
||||
if String(row.get("target", "")) == "cell":
|
||||
resolved.append(target)
|
||||
else:
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
var c := target + Vector2i(dx, dy)
|
||||
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
|
||||
resolved.append(c)
|
||||
return resolved
|
||||
|
||||
|
||||
## 预读死窗判定:纯预读行窗口全已开 = 没东西可看(cell_unrevealed_cb = 排雷玩法 只读面)
|
||||
func _preview_window_dead(resolved: Array, cell_unrevealed_cb: Callable) -> bool:
|
||||
var any_unrevealed := false
|
||||
if cell_unrevealed_cb.is_valid():
|
||||
for c in resolved:
|
||||
if bool(cell_unrevealed_cb.call(c)):
|
||||
any_unrevealed = true
|
||||
break
|
||||
return not any_unrevealed
|
||||
|
||||
|
||||
# ---- 叠加层(战斗结算逐 kind 应用——空间先、ap 后的序在编排侧钉死)----
|
||||
|
||||
func apply_overlay(kind: String, cells: Array) -> void:
|
||||
|
||||
+59
-30
@@ -33,42 +33,58 @@ func _init(db_ref: ConfigDb) -> void:
|
||||
# (下标=行集行序,长度短于行集时缺省 -1 = 该行行值路径;空数组 = 全行定值)。
|
||||
# 逐行实例化、生成序 = 文件行序(行序即契约,敌人系统 v0.15);行集实例总数(各行传入数量求和)
|
||||
# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。
|
||||
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [],
|
||||
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [],
|
||||
hero_attack_type: String = "", hero_armor_type: String = "", row_kind: String = "battle") -> bool:
|
||||
# spawn 引用束(K6′ 签名束化——10 形参收单 Dictionary 束):束键封闭词表——
|
||||
# 必载 level_index/node_depth/hero_hp;可选 unit_count_overrides=[]/enemy_atk_overrides=[]/
|
||||
# hero_atk_override=-1/hero_max_hp_override=-1/hero_attack_type=""/hero_armor_type=""/
|
||||
# row_kind="battle"(缺省语义与现行形参缺省逐字同值)。
|
||||
# enemy_atk_override:遭遇骰同流的 spawn 期整战持久 roll 结果
|
||||
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult)。敌人系统 零随机纪律保持。
|
||||
# 行集受理:unit_count_overrides / enemy_atk_overrides 为**逐行数组**
|
||||
# (下标=行集行序,长度短于行集时缺省 -1 = 该行行值路径;空数组 = 全行定值)。
|
||||
# 逐行实例化、生成序 = 文件行序(行序即契约,敌人系统 v0.15);行集实例总数(各行传入数量求和)
|
||||
# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。
|
||||
func spawn(req: Dictionary) -> bool:
|
||||
if _spawned:
|
||||
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
|
||||
return false
|
||||
# 行集数组口径(resolver 按 (L,band) 带寻址返回全部行、文件序)
|
||||
var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth, row_kind)
|
||||
var rowset: Array[Variant] = db.combat.enemy.resolve_rowset(int(req.level_index), int(req.node_depth), String(req.get("row_kind", "battle")))
|
||||
if rowset.is_empty():
|
||||
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d,%s)" % [level_index, node_depth, row_kind])
|
||||
push_error("[敌人系统] 遭遇行解析失败/越域 (%s,%s,%s)" % [str(req.get("level_index")), str(req.get("node_depth")), String(req.get("row_kind", "battle"))])
|
||||
assert(false, "遭遇行悬空(R1 前置守卫)")
|
||||
return false
|
||||
var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk)
|
||||
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp)
|
||||
if not _spawn_hero_unit(int(req.hero_hp), int(req.get("hero_atk_override", -1)), int(req.get("hero_max_hp_override", -1)), String(req.get("hero_attack_type", "")), String(req.get("hero_armor_type", ""))):
|
||||
return false
|
||||
var mod: Dictionary[Variant, Variant] = db.run.difficulty.resolve_modifiers(int(req.level_index), int(req.node_depth))
|
||||
if not _spawn_rowset(rowset, req.get("unit_count_overrides", []), req.get("enemy_atk_overrides", []), float(mod.get("hp_mult", 1.0)), float(mod.get("atk_mult", 1.0))):
|
||||
return false
|
||||
_spawned = true
|
||||
_despawned = false
|
||||
return true
|
||||
|
||||
|
||||
## 英雄单位构建域:生效上限与类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light)。
|
||||
## max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 HP 时上限不得压扁
|
||||
## (恢复点/治疗包的满血守卫与治疗上限随之失效)
|
||||
func _spawn_hero_unit(hero_hp: int, atk_override: int, max_hp_override: int, hero_attack_type: String, hero_armor_type: String) -> bool:
|
||||
var eff_atk: int = atk_override if atk_override >= 1 else int(db.combat.hero.atk)
|
||||
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.combat.hero.max_hp)
|
||||
if hero_hp > eff_max_hp or hero_hp <= 0:
|
||||
push_warning("[敌人系统] hero_hp 入参越界(EC16): %d" % hero_hp)
|
||||
return false
|
||||
# max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 HP 时上限不得压扁
|
||||
# (恢复点/治疗包的满血守卫与治疗上限随之失效)
|
||||
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": String(db.hero.display_name), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面)
|
||||
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰:施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15)
|
||||
var mod: Dictionary[Variant, Variant] = db.resolve_difficulty_modifiers(level_index, node_depth)
|
||||
var hp_mult: float = float(mod.get("hp_mult", 1.0))
|
||||
var atk_mult: float = float(mod.get("atk_mult", 1.0))
|
||||
# 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值;
|
||||
# 行 i 数量 = overrides[i](≥1 时)否则该行 unit_count 行值)
|
||||
var per_row_counts: Array[int] = []
|
||||
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": String(db.combat.hero.display_name), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.crossrun.forge.default_armor_type, "attack_type": hero_attack_type if hero_attack_type != "" else db.crossrun.forge.default_attack_type} # 护甲/攻击类型轴快照(甲效矩阵消费面)
|
||||
return true
|
||||
|
||||
|
||||
## 行集实例化域(难度曲线修饰施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为〔AC-10〕;
|
||||
## 混编行集内全部敌种同乘——(L,D) 精确匹配一次取值,F9 同乘不改变异,敌人系统 v0.15)
|
||||
func _spawn_rowset(rowset: Array, unit_count_overrides: Array, enemy_atk_overrides: Array, hp_mult: float, atk_mult: float) -> bool:
|
||||
_enemies.clear()
|
||||
var per_row_counts := _row_instance_counts(rowset, unit_count_overrides)
|
||||
if per_row_counts.is_empty():
|
||||
return false
|
||||
var total_instances := 0
|
||||
for row_i in range(rowset.size()):
|
||||
var row_i_dict: Dictionary[Variant, Variant] = rowset[row_i]
|
||||
var ovr: int = int(unit_count_overrides[row_i]) if row_i < unit_count_overrides.size() else -1
|
||||
var count: int = ovr if ovr >= 1 else int(row_i_dict.unit_count)
|
||||
if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX:
|
||||
push_warning("[敌人系统] 行 %d 实例化数量越域(单种数域 [%d,%d]): %d" % [row_i, EnemyTable.UNIT_COUNT_MIN, EnemyTable.UNIT_COUNT_MAX, count])
|
||||
return false
|
||||
per_row_counts.append(count)
|
||||
for count in per_row_counts:
|
||||
total_instances += count
|
||||
# 行集实例总数守卫(行集构成纪律③——各行骰后数量求和 ≤4;骰后值加载期不可判,此处 fail-fast)
|
||||
if total_instances > EnemyTable.ROWSET_INSTANCE_MAX:
|
||||
@@ -99,11 +115,24 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
|
||||
"is_hero": false,
|
||||
})
|
||||
handle += 1
|
||||
_spawned = true
|
||||
_despawned = false
|
||||
return true
|
||||
|
||||
|
||||
## 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值;
|
||||
## 行 i 数量 = overrides[i](≥1 时)否则该行 unit_count 行值)。越域行 = 空数组(受理拒绝信号)
|
||||
func _row_instance_counts(rowset: Array, unit_count_overrides: Array) -> Array[int]:
|
||||
var per_row_counts: Array[int] = []
|
||||
for row_i in range(rowset.size()):
|
||||
var row_i_dict: Dictionary[Variant, Variant] = rowset[row_i]
|
||||
var ovr: int = int(unit_count_overrides[row_i]) if row_i < unit_count_overrides.size() else -1
|
||||
var count: int = ovr if ovr >= 1 else int(row_i_dict.unit_count)
|
||||
if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX:
|
||||
push_warning("[敌人系统] 行 %d 实例化数量越域(单种数域 [%d,%d]): %d" % [row_i, EnemyTable.UNIT_COUNT_MIN, EnemyTable.UNIT_COUNT_MAX, count])
|
||||
return []
|
||||
per_row_counts.append(count)
|
||||
return per_row_counts
|
||||
|
||||
|
||||
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条
|
||||
# 快照纪律。触发条件:技能行 trigger 是字段权威——快照随读;带 latch 行写单向闩标记
|
||||
# trigger_latched 初始锁定;缺省无 trigger=无条件技)
|
||||
@@ -115,7 +144,7 @@ func _pool_snapshot(row: Dictionary) -> Array[Dictionary]:
|
||||
"weight": int(entry.get("weight", 0)),
|
||||
"cooldown_left": 0,
|
||||
}
|
||||
var srow_snap: Dictionary[Variant, Variant] = db.skill_row(String(entry.get("skill_id", "")))
|
||||
var srow_snap: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(String(entry.get("skill_id", "")))
|
||||
if srow_snap.has("trigger"):
|
||||
snap_entry["trigger"] = (srow_snap.get("trigger", {}) as Dictionary).duplicate(true)
|
||||
if bool((snap_entry["trigger"] as Dictionary).get("latch", false)):
|
||||
@@ -218,7 +247,7 @@ func special_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
|
||||
for e in u.pool:
|
||||
if _entry_blocked(e, u, facts):
|
||||
continue
|
||||
var srow: Dictionary[Variant, Variant] = db.skill_row(String(e.skill_id))
|
||||
var srow: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(String(e.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) != "basic":
|
||||
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
|
||||
return out
|
||||
@@ -276,7 +305,7 @@ func commit_action(u: Dictionary) -> void:
|
||||
if sid == "":
|
||||
_diag("commit_action 无待发动作(兑现序违例)")
|
||||
return
|
||||
var srow: Dictionary[Variant, Variant] = db.skill_row(sid)
|
||||
var srow: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
|
||||
var cd: int = int(srow.get("cooldown_beats", 0)) if not srow.is_empty() else 0
|
||||
for e in u.pool:
|
||||
if String(e.skill_id) == sid:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
## 冒险表·已拿上限段(K4′ 拆段——curio_cap_* 9 槽随段件嵌套;段内槽名逐字不动;
|
||||
## 概率与上限解耦〔奇物.md 池结构案D〕——概率位=档+令牌数、上限=本字段〔拿了才计、刷出不计〕;
|
||||
## cap 满后该 kind 从候选视图剔除〔提前退役〕。99=实质无上限占位,实数归调参;专长 cap 在 feats.tres 行字段)。
|
||||
class_name AdventureCapSegment
|
||||
extends Resource
|
||||
|
||||
@export var curio_cap_attack: int = 99 # [1,99]
|
||||
@export var curio_cap_max_hp: int = 99 # [1,99]
|
||||
@export var curio_cap_armor: int = 99 # [1,99]
|
||||
@export var curio_cap_gp: int = 99 # [1,99]
|
||||
@export var curio_cap_ap: int = 99 # [1,99]
|
||||
@export var curio_cap_heal: int = 99 # [1,99]
|
||||
@export var curio_cap_refresh: int = 99 # [1,99]
|
||||
@export var curio_cap_rap: int = 99 # [1,99]
|
||||
@export var curio_cap_time_sand: int = 99 # [1,99]
|
||||
@@ -0,0 +1 @@
|
||||
uid://b88nnsq53mium
|
||||
@@ -0,0 +1,18 @@
|
||||
## 冒险表·令牌配额段(K4′ 拆段——curio_quota_* 13 槽随段件嵌套;段内槽名逐字不动;
|
||||
## 嵌套段单 .tres 保持——形态同 settlement_table 拆段既裁)。kind/tier 词表与配额结构锚留宿主。
|
||||
class_name AdventureQuotaSegment
|
||||
extends Resource
|
||||
|
||||
@export var curio_quota_attack_common: int = 18 # [0,99]
|
||||
@export var curio_quota_attack_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_max_hp_common: int = 17 # [0,99]
|
||||
@export var curio_quota_max_hp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_armor_common: int = 17 # [0,99]
|
||||
@export var curio_quota_armor_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_gp_common: int = 18 # [0,99]
|
||||
@export var curio_quota_gp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_heal_excellent: int = 3 # [0,99]——恢复件(refresh 挤占 2 席位,总量 102 锚不变)
|
||||
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(补给类成员;provisional 归曲线调参)
|
||||
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档)
|
||||
@export var curio_quota_rap_rare: int = 2 # [0,99]——储备金袋(投放 provisional 归调参)
|
||||
@export var curio_quota_time_sand_rare: int = 2 # [0,99]——时沙(投放 provisional 归调参)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi52mqsld3mpc
|
||||
@@ -45,49 +45,11 @@ const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 7, "legendary": 0}
|
||||
@export var shop_refresh_cost_base: int = 20 # [1,999] knob(MC-1 provisional——GP 刷新基价)
|
||||
@export var shop_refresh_cost_curve: float = 1.8 # [1.0,4.0] knob(MC-1 provisional——按已付费刷新数幂曲线,A 链同族)
|
||||
|
||||
# —— 令牌配额(kind × tier 计数;未列组合 = 0。四级档名 common/excellent/rare/legendary
|
||||
# 承袭同一物理位序〔70/25/7/0〕;各 kind 现值见行注默认)——
|
||||
@export var curio_quota_attack_common: int = 18 # [0,99]
|
||||
@export var curio_quota_attack_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_max_hp_common: int = 17 # [0,99]
|
||||
@export var curio_quota_max_hp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_armor_common: int = 17 # [0,99]
|
||||
@export var curio_quota_armor_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_gp_common: int = 18 # [0,99]
|
||||
@export var curio_quota_gp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_heal_excellent: int = 3 # [0,99]——恢复件(refresh 挤占 2 席位,总量 102 锚不变)
|
||||
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(补给类成员;provisional 归曲线调参)
|
||||
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档)
|
||||
@export var curio_quota_rap_rare: int = 2 # [0,99]——储备金袋(投放 provisional 归调参)
|
||||
@export var curio_quota_time_sand_rare: int = 2 # [0,99]——时沙(投放 provisional 归调参)
|
||||
|
||||
# —— 令牌件值(小/大数值对——v2.2 令牌池口径定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)——
|
||||
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
|
||||
@export var curio_value_attack_excellent: int = 2 # [0,999] knob——大增伤
|
||||
@export var curio_value_max_hp_common: int = 5 # [0,999] knob——小生命
|
||||
@export var curio_value_max_hp_excellent: int = 10 # [0,999] knob——大生命
|
||||
@export var curio_value_armor_common: int = 1 # [0,999] knob——小护甲
|
||||
@export var curio_value_armor_excellent: int = 2 # [0,999] knob——大护甲
|
||||
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
|
||||
@export var curio_value_gp_excellent: int = 50 # [0,999] knob——大GP
|
||||
@export var curio_value_heal_excellent: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
|
||||
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
|
||||
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
|
||||
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
|
||||
|
||||
# —— 已拿上限 cap(概率与上限解耦〔奇物.md 池结构案D〕——案②令牌池+提前退役:
|
||||
# 概率位=档+令牌数〔牌库物理张数〕、上限=本字段〔拿了才计、刷出不计〕;cap 满后该 kind 从候选
|
||||
# 视图剔除〔提前退役〕。99=实质无上限占位,实数归调参;专长 cap 在 feats.tres 行字段〕)——
|
||||
@export var curio_cap_attack: int = 99 # [1,99]
|
||||
@export var curio_cap_max_hp: int = 99 # [1,99]
|
||||
@export var curio_cap_armor: int = 99 # [1,99]
|
||||
@export var curio_cap_gp: int = 99 # [1,99]
|
||||
@export var curio_cap_ap: int = 99 # [1,99]
|
||||
@export var curio_cap_heal: int = 99 # [1,99]
|
||||
@export var curio_cap_refresh: int = 99 # [1,99]
|
||||
@export var curio_cap_rap: int = 99 # [1,99]
|
||||
@export var curio_cap_time_sand: int = 99 # [1,99]
|
||||
# —— 列族三段件(K4′ 拆段——36 个列族标量槽随段件嵌套,段内槽名逐字不动;
|
||||
# 嵌套段单 .tres 保持——形态同 settlement_table 拆段既裁)——
|
||||
@export var quota_segment: AdventureQuotaSegment = AdventureQuotaSegment.new() # 令牌配额段(curio_quota_* 13)
|
||||
@export var value_segment: AdventureValueSegment = AdventureValueSegment.new() # 令牌件值段(curio_value_* 13)
|
||||
@export var cap_segment: AdventureCapSegment = AdventureCapSegment.new() # 已拿上限段(curio_cap_* 9)
|
||||
|
||||
# —— 专长获取专长凭证(非 Boss 渠道的专长入口形态,奇物.md v2.7 渠道轴定位;
|
||||
# 四口径契约④:拿到即开不可囤积/罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
|
||||
@@ -120,53 +82,53 @@ func curio_apply_route(kind: String) -> String:
|
||||
## heal/refresh excellent、ap rare、凭证 kind rare 档)
|
||||
func curio_quota(kind: String, tier: String) -> int:
|
||||
match [kind, tier]:
|
||||
["attack", TIER_COMMON]: return curio_quota_attack_common
|
||||
["attack", TIER_EXCELLENT]: return curio_quota_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return curio_quota_max_hp_common
|
||||
["max_hp", TIER_EXCELLENT]: return curio_quota_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return curio_quota_armor_common
|
||||
["armor", TIER_EXCELLENT]: return curio_quota_armor_excellent
|
||||
["gp", TIER_COMMON]: return curio_quota_gp_common
|
||||
["gp", TIER_EXCELLENT]: return curio_quota_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return curio_quota_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return curio_quota_refresh_excellent
|
||||
["ap", TIER_RARE]: return curio_quota_ap_rare
|
||||
["rap", TIER_RARE]: return curio_quota_rap_rare
|
||||
["time_sand", TIER_RARE]: return curio_quota_time_sand_rare
|
||||
["attack", TIER_COMMON]: return quota_segment.curio_quota_attack_common
|
||||
["attack", TIER_EXCELLENT]: return quota_segment.curio_quota_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return quota_segment.curio_quota_max_hp_common
|
||||
["max_hp", TIER_EXCELLENT]: return quota_segment.curio_quota_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return quota_segment.curio_quota_armor_common
|
||||
["armor", TIER_EXCELLENT]: return quota_segment.curio_quota_armor_excellent
|
||||
["gp", TIER_COMMON]: return quota_segment.curio_quota_gp_common
|
||||
["gp", TIER_EXCELLENT]: return quota_segment.curio_quota_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return quota_segment.curio_quota_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return quota_segment.curio_quota_refresh_excellent
|
||||
["ap", TIER_RARE]: return quota_segment.curio_quota_ap_rare
|
||||
["rap", TIER_RARE]: return quota_segment.curio_quota_rap_rare
|
||||
["time_sand", TIER_RARE]: return quota_segment.curio_quota_time_sand_rare
|
||||
return 0
|
||||
|
||||
|
||||
## 令牌件值(kind × tier;四级构成随配额表)
|
||||
func curio_value(kind: String, tier: String = TIER_COMMON) -> int:
|
||||
match [kind, tier]:
|
||||
["attack", TIER_COMMON]: return curio_value_attack_common
|
||||
["attack", TIER_EXCELLENT]: return curio_value_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return curio_value_max_hp_common
|
||||
["max_hp", TIER_EXCELLENT]: return curio_value_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return curio_value_armor_common
|
||||
["armor", TIER_EXCELLENT]: return curio_value_armor_excellent
|
||||
["gp", TIER_COMMON]: return curio_value_gp_common
|
||||
["gp", TIER_EXCELLENT]: return curio_value_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return curio_value_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return curio_value_refresh_excellent
|
||||
["ap", TIER_RARE]: return curio_value_ap_rare
|
||||
["rap", TIER_RARE]: return curio_value_rap_rare
|
||||
["time_sand", TIER_RARE]: return curio_value_time_sand_rare
|
||||
["attack", TIER_COMMON]: return value_segment.curio_value_attack_common
|
||||
["attack", TIER_EXCELLENT]: return value_segment.curio_value_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return value_segment.curio_value_max_hp_common
|
||||
["max_hp", TIER_EXCELLENT]: return value_segment.curio_value_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return value_segment.curio_value_armor_common
|
||||
["armor", TIER_EXCELLENT]: return value_segment.curio_value_armor_excellent
|
||||
["gp", TIER_COMMON]: return value_segment.curio_value_gp_common
|
||||
["gp", TIER_EXCELLENT]: return value_segment.curio_value_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return value_segment.curio_value_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return value_segment.curio_value_refresh_excellent
|
||||
["ap", TIER_RARE]: return value_segment.curio_value_ap_rare
|
||||
["rap", TIER_RARE]: return value_segment.curio_value_rap_rare
|
||||
["time_sand", TIER_RARE]: return value_segment.curio_value_time_sand_rare
|
||||
return 0
|
||||
|
||||
|
||||
## 已拿上限读数(案②——kind 维度;专长 cap 走 feats.tres 行字段不走本面)
|
||||
func curio_cap(kind: String) -> int:
|
||||
match kind:
|
||||
"attack": return curio_cap_attack
|
||||
"max_hp": return curio_cap_max_hp
|
||||
"armor": return curio_cap_armor
|
||||
"gp": return curio_cap_gp
|
||||
"ap": return curio_cap_ap
|
||||
"heal": return curio_cap_heal
|
||||
"refresh": return curio_cap_refresh
|
||||
"rap": return curio_cap_rap
|
||||
"time_sand": return curio_cap_time_sand
|
||||
"attack": return cap_segment.curio_cap_attack
|
||||
"max_hp": return cap_segment.curio_cap_max_hp
|
||||
"armor": return cap_segment.curio_cap_armor
|
||||
"gp": return cap_segment.curio_cap_gp
|
||||
"ap": return cap_segment.curio_cap_ap
|
||||
"heal": return cap_segment.curio_cap_heal
|
||||
"refresh": return cap_segment.curio_cap_refresh
|
||||
"rap": return cap_segment.curio_cap_rap
|
||||
"time_sand": return cap_segment.curio_cap_time_sand
|
||||
return 99
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
## 冒险表·令牌件值段(K4′ 拆段——curio_value_* 13 槽随段件嵌套;段内槽名逐字不动;
|
||||
## 小/大数值对——v2.2 令牌池口径定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)。
|
||||
class_name AdventureValueSegment
|
||||
extends Resource
|
||||
|
||||
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
|
||||
@export var curio_value_attack_excellent: int = 2 # [0,999] knob——大增伤
|
||||
@export var curio_value_max_hp_common: int = 5 # [0,999] knob——小生命
|
||||
@export var curio_value_max_hp_excellent: int = 10 # [0,999] knob——大生命
|
||||
@export var curio_value_armor_common: int = 1 # [0,999] knob——小护甲
|
||||
@export var curio_value_armor_excellent: int = 2 # [0,999] knob——大护甲
|
||||
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
|
||||
@export var curio_value_gp_excellent: int = 50 # [0,999] knob——大GP
|
||||
@export var curio_value_heal_excellent: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
|
||||
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
|
||||
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
|
||||
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0y3hcpqyv1li
|
||||
@@ -19,63 +19,100 @@ func validate() -> Array:
|
||||
var levels := {}
|
||||
var anchor_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var key := "%d_%d" % [L, D]
|
||||
if levels.has(key):
|
||||
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
|
||||
levels[key] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
|
||||
if D < 0:
|
||||
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
if w < 8 or w > 64 or h < 8 or h > 64:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
if d <= 0.0 or d > 0.5:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
|
||||
var t: float = float(row.get("inferability_target", 0.0))
|
||||
if t < TARGET_ABS_FLOOR or t > 1.0:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
|
||||
if not SOLVER_RULES.has(row.get("solver_rules", "")):
|
||||
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
|
||||
if row.get("solver_rules", "") != "single_subset":
|
||||
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
|
||||
var ms: int = int(row.get("min_safe_count", 0))
|
||||
if ms < 1 or ms > 999:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
# 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
|
||||
var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
if w * h - n_mines < ms:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
|
||||
# F2c 成本可行性
|
||||
if att * w * h > COST_BUDGET:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
|
||||
# 图类型词表 v1.0(dungeon=地牢基线 / underdark=幽暗地域;扩词走本表修订)
|
||||
var bt: String = str(row.get("board_type", ""))
|
||||
if not BOARD_TYPE_VOCABULARY.has(bt):
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
|
||||
# 初始展开收窄两参数(加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
|
||||
var sr: int = int(row.get("shave_ring_radius", 1))
|
||||
if sr < 0 or sr > 2:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
|
||||
var r0_cap: int = int(row.get("r0_max_cap", 0))
|
||||
if r0_cap > 0:
|
||||
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
|
||||
if r0_cap < BoardData.SPAWN_AREA_CELLS:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
|
||||
if r0_cap > (w * h) / 2:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
errs.append_array(_validate_row(row, levels, anchor_ok))
|
||||
errs.append_array(_validate_row_constraints(row))
|
||||
errs.append_array(_validate_shave_params(row))
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "level %d 缺 depth-0 锚行(EC15)" % L})
|
||||
return errs
|
||||
|
||||
|
||||
## 行域:键唯一与进度/尺寸/密度/推理目标/solver 规则冻结(levels/anchor_ok 引用累计)
|
||||
func _validate_row(row: Dictionary, levels: Dictionary, anchor_ok: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var key := "%d_%d" % [L, D]
|
||||
if levels.has(key):
|
||||
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
|
||||
levels[key] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
|
||||
if D < 0:
|
||||
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
if w < 8 or w > 64 or h < 8 or h > 64:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
if d <= 0.0 or d > 0.5:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
|
||||
var t: float = float(row.get("inferability_target", 0.0))
|
||||
if t < TARGET_ABS_FLOOR or t > 1.0:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
|
||||
if not SOLVER_RULES.has(row.get("solver_rules", "")):
|
||||
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
|
||||
if row.get("solver_rules", "") != "single_subset":
|
||||
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
return errs
|
||||
|
||||
|
||||
## 约束域:min_safe/生成尝试预算/布雷数下限与可行性/成本护栏/图类型词表
|
||||
func _validate_row_constraints(row: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
var ms: int = int(row.get("min_safe_count", 0))
|
||||
if ms < 1 or ms > 999:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
# 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
|
||||
var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
if w * h - n_mines < ms:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
|
||||
# F2c 成本可行性
|
||||
if att * w * h > COST_BUDGET:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
|
||||
# 图类型词表 v1.0(dungeon=地牢基线 / underdark=幽暗地域;扩词走本表修订)
|
||||
var bt: String = str(row.get("board_type", ""))
|
||||
if not BOARD_TYPE_VOCABULARY.has(bt):
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
|
||||
return errs
|
||||
|
||||
|
||||
## 初始展开收窄域(加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
|
||||
func _validate_shave_params(row: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
var sr: int = int(row.get("shave_ring_radius", 1))
|
||||
if sr < 0 or sr > 2:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
|
||||
var r0_cap: int = int(row.get("r0_max_cap", 0))
|
||||
if r0_cap > 0:
|
||||
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
|
||||
if r0_cap < BoardData.SPAWN_AREA_CELLS:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
|
||||
if r0_cap > (w * h) / 2:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
|
||||
return errs
|
||||
|
||||
|
||||
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
|
||||
|
||||
|
||||
## 进度行解析委托(消费面 db.combat.board_gen.resolve_row——R1 ceil/clamp 语义归 ConfigDbResolver)
|
||||
func resolve_row(L: int, D: int) -> Dictionary:
|
||||
return _resolver.resolve_board_gen(L, D)
|
||||
@@ -28,3 +28,11 @@ func validate() -> Array:
|
||||
if v < MULT_MIN or v > MULT_MAX:
|
||||
errs.append({"category": "mult_oor_warning", "message": "%s (%s) 越域 [0.1,10] 使用时 clamp: %f" % [key, k, v]})
|
||||
return errs
|
||||
|
||||
|
||||
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
|
||||
|
||||
|
||||
## 难度修饰解析委托(消费面 db.run.difficulty.resolve_modifiers——clamp 语义归 ConfigDbResolver)
|
||||
func resolve_modifiers(L: int, D: int) -> Dictionary:
|
||||
return _resolver.resolve_difficulty_modifiers(L, D)
|
||||
@@ -44,98 +44,147 @@ func validate() -> Array:
|
||||
var anchor_ok := {}
|
||||
var boss_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var rk: String = String(row.get("row_kind", ""))
|
||||
var band: int = int(row.get("depth_band", -1))
|
||||
var eid: String = row.get("enemy_id", "")
|
||||
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or band < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
|
||||
if not ROW_KINDS.has(rk):
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]})
|
||||
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]})
|
||||
var hp: int = int(row.get("hp", 0))
|
||||
if hp < 1 or hp > 999:
|
||||
errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid})
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
if atk < 0 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
|
||||
# atk_min/atk_max(可省略配对):遭遇骰同流 spawn 期整战持久 roll;
|
||||
# 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
var has_amin: bool = row.has("atk_min")
|
||||
var has_amax: bool = row.has("atk_max")
|
||||
if has_amin != has_amax:
|
||||
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
|
||||
elif has_amin:
|
||||
var amin: int = int(row.get("atk_min", 0))
|
||||
var amax: int = int(row.get("atk_max", 0))
|
||||
if amin < 0 or amax > 999 or amin > amax:
|
||||
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
|
||||
if atk != int((amin + amax) / 2.0):
|
||||
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
|
||||
# 池模板(单表结构校验;跨表构成纪律在 ConfigDB):
|
||||
# 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
|
||||
# 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
|
||||
var pool: Array[Variant] = row.get("pool", [])
|
||||
if pool.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
|
||||
var pool_ids := {}
|
||||
for entry_v in pool:
|
||||
if not (entry_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
|
||||
continue
|
||||
var entry: Dictionary[Variant, Variant] = entry_v
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
|
||||
elif pool_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
|
||||
pool_ids[sid] = true
|
||||
var w: int = int(entry.get("weight", 0))
|
||||
if w <= 0 or w > 9999:
|
||||
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
|
||||
# armor_type(可省略——缺省 light 向后兼容)
|
||||
var at: String = String(row.get("armor_type", "light"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
|
||||
# attack_type(可省略,缺省 normal 向后兼容;写法沿用 armor_type 同款)
|
||||
var att: String = String(row.get("attack_type", "normal"))
|
||||
if not ATTACK_TYPES.has(att):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]})
|
||||
# armor_class(可省略——缺省 0 向后兼容;防守方掷点判定阈值基数)
|
||||
var ac: int = int(row.get("armor_class", 0))
|
||||
if ac < 0 or ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
|
||||
errs.append_array(_validate_row_base(row, seen))
|
||||
var eid2 := String(row.get("enemy_id", ""))
|
||||
errs.append_array(_validate_row_numbers(row, eid2))
|
||||
errs.append_array(_validate_row_atk_range(row, eid2))
|
||||
errs.append_array(_validate_row_pool(row, eid2))
|
||||
errs.append_array(_validate_row_types(row, eid2))
|
||||
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须
|
||||
# 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。
|
||||
# 同带键多敌种混编合法;同带键同敌种第二行仍由
|
||||
# dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。
|
||||
var rowset_key := "%d_%s_%d" % [L, rk, band]
|
||||
var rowset_key := "%d_%s_%d" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]
|
||||
if rowset_key != last_rowset_key:
|
||||
if closed_rowset.has(rowset_key):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, rk, band]})
|
||||
errs.append_array(_validate_rowset_closed(rowset_key, closed_rowset, row))
|
||||
closed_rowset[rowset_key] = true
|
||||
last_rowset_key = rowset_key
|
||||
if rk == "battle" and band == 0:
|
||||
anchor_ok[L] = true
|
||||
elif rk == "boss":
|
||||
boss_ok[L] = true
|
||||
if not anchor_ok.has(L):
|
||||
anchor_ok[L] = false
|
||||
var L2: int = int(row.get("level_index", 0))
|
||||
var rk2: String = String(row.get("row_kind", ""))
|
||||
if rk2 == "battle" and int(row.get("depth_band", -1)) == 0:
|
||||
anchor_ok[L2] = true
|
||||
elif rk2 == "boss":
|
||||
boss_ok[L2] = true
|
||||
if not anchor_ok.has(L2):
|
||||
anchor_ok[L2] = false
|
||||
errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok))
|
||||
return errs
|
||||
|
||||
|
||||
## 行头基面域:键提取/查重与登记/进度键越域/row_kind 枚举/enemy_id 非空/display_name
|
||||
func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var rk: String = String(row.get("row_kind", ""))
|
||||
var band: int = int(row.get("depth_band", -1))
|
||||
var eid: String = row.get("enemy_id", "")
|
||||
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or band < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
|
||||
if not ROW_KINDS.has(rk):
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
return errs
|
||||
|
||||
|
||||
## 数值域:unit_count(含可省略上限)/hp/atk 基值
|
||||
func _validate_row_numbers(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]})
|
||||
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]})
|
||||
var hp: int = int(row.get("hp", 0))
|
||||
if hp < 1 or hp > 999:
|
||||
errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid})
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
if atk < 0 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
|
||||
return errs
|
||||
|
||||
|
||||
## atk 区间域(可省略配对):遭遇骰同流 spawn 期整战持久 roll;
|
||||
## 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
var has_amin: bool = row.has("atk_min")
|
||||
var has_amax: bool = row.has("atk_max")
|
||||
if has_amin != has_amax:
|
||||
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
|
||||
elif has_amin:
|
||||
var amin: int = int(row.get("atk_min", 0))
|
||||
var amax: int = int(row.get("atk_max", 0))
|
||||
if amin < 0 or amax > 999 or amin > amax:
|
||||
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
|
||||
if atk != int((amin + amax) / 2.0):
|
||||
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
|
||||
return errs
|
||||
|
||||
|
||||
## 池模板域(单表结构校验;跨表构成纪律在 ConfigDB):
|
||||
## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
|
||||
## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
|
||||
func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var pool: Array[Variant] = row.get("pool", [])
|
||||
if pool.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
|
||||
var pool_ids := {}
|
||||
for entry_v in pool:
|
||||
if not (entry_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
|
||||
continue
|
||||
var entry: Dictionary[Variant, Variant] = entry_v
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
|
||||
elif pool_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
|
||||
pool_ids[sid] = true
|
||||
var w: int = int(entry.get("weight", 0))
|
||||
if w <= 0 or w > 9999:
|
||||
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
|
||||
return errs
|
||||
|
||||
|
||||
## 攻防类型域(均可省略——armor_type 缺省 light/attack_type 缺省 normal/armor_class 缺省 0 向后兼容)
|
||||
func _validate_row_types(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var at: String = String(row.get("armor_type", "light"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
|
||||
var att: String = String(row.get("attack_type", "normal"))
|
||||
if not ATTACK_TYPES.has(att):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]})
|
||||
# armor_class(防守方掷点判定阈值基数)
|
||||
var ac: int = int(row.get("armor_class", 0))
|
||||
if ac < 0 or ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
|
||||
return errs
|
||||
|
||||
|
||||
## 行序违例域:当前行集键曾出现且 ≠ 上一行行集键 = 同带键行被隔断(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)
|
||||
func _validate_rowset_closed(rowset_key: String, closed_rowset: Dictionary, row: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if closed_rowset.has(rowset_key):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]})
|
||||
return errs
|
||||
|
||||
|
||||
## 锚行尾段:每 level 必有 (battle, band 0) 锚行与 Boss 行集
|
||||
func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 (battle, band 0) 锚行" % L})
|
||||
@@ -158,3 +207,11 @@ static func band_of(band_keys: Array, D: int) -> int:
|
||||
# 进度键解析单点收口(全仓统走
|
||||
# ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧),
|
||||
# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。
|
||||
|
||||
|
||||
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
|
||||
|
||||
|
||||
## 带寻址行集解析委托(消费面 db.combat.enemy.resolve_rowset——D 维 band_of 落带语义归 ConfigDbResolver)
|
||||
func resolve_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
|
||||
return _resolver.resolve_enemy_rowset(L, D, row_kind)
|
||||
@@ -45,66 +45,101 @@ func validate() -> Array:
|
||||
for row in routes: # 预扫:route_id→kind 全集(前置反查需前向引用——轻甲行在长枪行之后)
|
||||
kind_by_id[String(row.get("route_id", ""))] = String(row.get("kind", ""))
|
||||
for row in routes:
|
||||
var rid := String(row.get("route_id", ""))
|
||||
if not ROUTE_IDS.has(rid):
|
||||
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var kind := String(row.get("kind", ""))
|
||||
if not ["weapon", "armor"].has(kind):
|
||||
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
|
||||
var head_out := _validate_route_head(row, seen, type_seen)
|
||||
errs.append_array(head_out["errs"])
|
||||
if head_out["skip"]:
|
||||
continue
|
||||
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
|
||||
var axis := String(row.get("type_axis", ""))
|
||||
var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
|
||||
if not domain.has(axis):
|
||||
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
|
||||
var type_key := "%s|%s" % [kind, axis]
|
||||
if type_seen.has(type_key):
|
||||
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
|
||||
type_seen[type_key] = true
|
||||
# 等级/增量/价格/基础 AC 值域
|
||||
var cap_v: int = int(row.get("cap", -1))
|
||||
if cap_v < 1 or cap_v > 9:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
|
||||
var gain: int = int(row.get("per_level_gain", -1))
|
||||
if gain < 0 or gain > 9:
|
||||
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
|
||||
var base_ac: int = int(row.get("base_ac", -1))
|
||||
if kind == "armor":
|
||||
if base_ac < 0 or base_ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
|
||||
elif base_ac != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
|
||||
var base_price: int = int(row.get("base_price", -1))
|
||||
if base_price < 1 or base_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
|
||||
var ratio: int = int(row.get("price_ratio_pct", -1))
|
||||
if ratio < 100 or ratio > 500:
|
||||
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
|
||||
var unlock_price: int = int(row.get("unlock_price", -1))
|
||||
if unlock_price < 0 or unlock_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
|
||||
# 解锁通道(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置
|
||||
var initial: bool = bool(row.get("initial_owned", false))
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
if initial:
|
||||
if prereq != "":
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
|
||||
if unlock_price != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
|
||||
initial_ids[rid] = kind
|
||||
else:
|
||||
if unlock_price < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
|
||||
if not ROUTE_IDS.has(prereq):
|
||||
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
|
||||
elif prereq == rid:
|
||||
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
|
||||
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
|
||||
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
|
||||
# 初始持有恰两行且一武器一甲(长剑/轻甲)
|
||||
var rid := String(row.get("route_id", ""))
|
||||
errs.append_array(_validate_route_values(row, rid))
|
||||
errs.append_array(_validate_route_unlock(row, rid, kind_by_id, initial_ids))
|
||||
errs.append_array(_validate_initial_coverage(initial_ids))
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:route_id 词表与查重/kind 出域(skip=跳过行剩余判据)/类型轴域与每型恰一路线
|
||||
func _validate_route_head(row: Dictionary, seen: Dictionary, type_seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skip := false
|
||||
var rid := String(row.get("route_id", ""))
|
||||
if not ROUTE_IDS.has(rid):
|
||||
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var kind := String(row.get("kind", ""))
|
||||
if not ["weapon", "armor"].has(kind):
|
||||
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
|
||||
skip = true
|
||||
return {"errs": errs, "skip": skip}
|
||||
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
|
||||
var axis := String(row.get("type_axis", ""))
|
||||
var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
|
||||
if not domain.has(axis):
|
||||
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
|
||||
var type_key := "%s|%s" % [kind, axis]
|
||||
if type_seen.has(type_key):
|
||||
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
|
||||
type_seen[type_key] = true
|
||||
return {"errs": errs, "skip": skip}
|
||||
|
||||
|
||||
## 数值域:等级/增量/价格/基础 AC 值域(甲型基础 AC 只属护甲路线)
|
||||
func _validate_route_values(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var kind := String(row.get("kind", ""))
|
||||
var cap_v: int = int(row.get("cap", -1))
|
||||
if cap_v < 1 or cap_v > 9:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
|
||||
var gain: int = int(row.get("per_level_gain", -1))
|
||||
if gain < 0 or gain > 9:
|
||||
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
|
||||
var base_ac: int = int(row.get("base_ac", -1))
|
||||
if kind == "armor":
|
||||
if base_ac < 0 or base_ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
|
||||
elif base_ac != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
|
||||
var base_price: int = int(row.get("base_price", -1))
|
||||
if base_price < 1 or base_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
|
||||
var ratio: int = int(row.get("price_ratio_pct", -1))
|
||||
if ratio < 100 or ratio > 500:
|
||||
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
|
||||
var unlock_price: int = int(row.get("unlock_price", -1))
|
||||
if unlock_price < 0 or unlock_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
|
||||
return errs
|
||||
|
||||
|
||||
## 解锁通道域(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置(武器←武器/甲←甲)
|
||||
func _validate_route_unlock(row: Dictionary, rid: String, kind_by_id: Dictionary, initial_ids: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var kind := String(row.get("kind", ""))
|
||||
var unlock_price: int = int(row.get("unlock_price", -1))
|
||||
var initial: bool = bool(row.get("initial_owned", false))
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
if initial:
|
||||
if prereq != "":
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
|
||||
if unlock_price != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
|
||||
initial_ids[rid] = kind
|
||||
else:
|
||||
if unlock_price < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
|
||||
if not ROUTE_IDS.has(prereq):
|
||||
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
|
||||
elif prereq == rid:
|
||||
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
|
||||
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
|
||||
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
|
||||
return errs
|
||||
|
||||
|
||||
## 初始持有覆盖域(两尾段):初始持有恰两行且一武器一甲(长剑/轻甲)+
|
||||
## 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
|
||||
func _validate_initial_coverage(initial_ids: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var init_weapons := 0
|
||||
var init_armors := 0
|
||||
for k in initial_ids.keys():
|
||||
@@ -114,7 +149,6 @@ func validate() -> Array:
|
||||
init_armors += 1
|
||||
if init_weapons != 1 or init_armors != 1:
|
||||
errs.append({"category": "coverage", "message": "初始持有须恰一武器一甲(现 sword/light): 武器 %d / 甲 %d" % [init_weapons, init_armors]})
|
||||
# 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
|
||||
if init_weapons == 1 and init_armors == 1:
|
||||
for row in routes:
|
||||
if not bool(row.get("initial_owned", false)):
|
||||
@@ -124,3 +158,27 @@ func validate() -> Array:
|
||||
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) != default_armor_type:
|
||||
errs.append({"category": "cross_field", "message": "default_armor_type 须=初始甲路线类型轴(裸体态轻甲): %s vs %s" % [default_armor_type, String(row.get("type_axis", ""))]})
|
||||
return errs
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位;消费面 db.crossrun.forge.<m> 直连)——
|
||||
# routes 属性在册故行族读取命名 route_rows(避开成员名冲突)----
|
||||
|
||||
## 六路线行族(副本——调用方禁改)
|
||||
func route_rows() -> Array:
|
||||
return routes.duplicate()
|
||||
|
||||
|
||||
## 路线行单点(键 route_id;缺行=空字典)
|
||||
func route(route_id: String) -> Dictionary:
|
||||
for row in routes:
|
||||
if String(row.get("route_id", "")) == route_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
|
||||
func armor_route_by_type(armor_type: String) -> Dictionary:
|
||||
for row in routes:
|
||||
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
|
||||
return row
|
||||
return {}
|
||||
@@ -1,5 +1,5 @@
|
||||
## 关卡节点图参数档 schema(节点图生成器核心批 卡① T1):五旋钮+战斗保底+岔口去路数的行式数据表。
|
||||
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.level_gen_params_row() 读取;生成器只吃参数档、不认关卡号(契约 6)。
|
||||
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.run.level_gen_params.row_by_level_index() 读取;生成器只吃参数档、不认关卡号(契约 6)。
|
||||
## 数值全 provisional(定值批落终值);域校验沿关卡节点图生成.md v1.1 调参表已锁条款——层深 L1≥6 递增增幅变大、
|
||||
## 配比四类合计 1、出口 L1 恒 1/L2-L3 1-3、岔口去路数恒 2/3、战斗保底≥0。
|
||||
class_name LevelGenParams
|
||||
@@ -17,50 +17,13 @@ func validate() -> Array:
|
||||
var seen := {}
|
||||
var layers_by_level := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var key := "level %d" % L
|
||||
if seen.has(L):
|
||||
errs.append({"category": "dup_key", "message": "LevelGenParams 键重复: %d" % L})
|
||||
seen[L] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "%s 关卡序号越域(≥1)" % key})
|
||||
var ks := (row as Dictionary).keys()
|
||||
ks.sort()
|
||||
var expect := ROW_KEYS.duplicate()
|
||||
expect.sort()
|
||||
if ks != expect:
|
||||
errs.append({"category": "structural", "message": "%s 字段键集非 ROW_KEYS 闭集" % key})
|
||||
var head := _validate_row_head(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
var layers: int = int(row.get("layers", 0))
|
||||
if layers < 6:
|
||||
errs.append({"category": "domain", "message": "%s 层深 %d 低于下限 6(层深 L1≥6 已锁)" % [key, layers]})
|
||||
layers_by_level[L] = layers
|
||||
var bd: float = float(row.get("branch_density", -1.0))
|
||||
if bd < 0.0 or bd > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
|
||||
var ratio_sum := 0.0
|
||||
for rk in RATIO_KEYS:
|
||||
var v: float = float(row.get(rk, -1.0))
|
||||
if v < 0.0 or v > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s %s=%f 越域 [0,1]" % [key, rk, v]})
|
||||
ratio_sum += v
|
||||
if not is_equal_approx(ratio_sum, 1.0):
|
||||
errs.append({"category": "domain", "message": "%s 配比四类合计 %f ≠ 1(已锁)" % [key, ratio_sum]})
|
||||
var entry_count: int = int(row.get("entry_count", 0))
|
||||
if entry_count < 1:
|
||||
errs.append({"category": "domain", "message": "%s 入口个数 %d 越域(≥1)" % [key, entry_count]})
|
||||
var exit_count: int = int(row.get("exit_count", 0))
|
||||
if exit_count < 1 or exit_count > 3:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d 越域 [1,3](已锁范围)" % [key, exit_count]})
|
||||
if L == 1 and exit_count != 1:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d ≠ 1(L1 出口恒 1 已锁)" % [key, exit_count]})
|
||||
var bf: int = int(row.get("battle_floor", -1))
|
||||
if bf < 0:
|
||||
errs.append({"category": "domain", "message": "%s 战斗保底 %d 越域(≥0)" % [key, bf]})
|
||||
var fmin: int = int(row.get("fork_ways_min", 0))
|
||||
var fmax: int = int(row.get("fork_ways_max", 0))
|
||||
if fmin != 2 or fmax != 3:
|
||||
errs.append({"category": "domain", "message": "%s 岔口去路数 (%d,%d) ≠ (2,3)(已锁)" % [key, fmin, fmax]})
|
||||
errs.append_array(_validate_row_shape(row, head["L"], head["key"], layers_by_level))
|
||||
errs.append_array(_validate_row_counts(row, head["L"], head["key"]))
|
||||
# 层深递进尾段(L1<L2<L3 且增幅变大——层深 L1≥6 递增增幅变大已锁)
|
||||
if layers_by_level.has(1) and layers_by_level.has(2) and layers_by_level.has(3):
|
||||
var d12: int = int(layers_by_level[2]) - int(layers_by_level[1])
|
||||
var d23: int = int(layers_by_level[3]) - int(layers_by_level[2])
|
||||
@@ -72,6 +35,68 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:level_index 查重/关卡序号越域/字段键集闭集(ok=跳过行剩余判据)
|
||||
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var key := "level %d" % L
|
||||
if seen.has(L):
|
||||
errs.append({"category": "dup_key", "message": "LevelGenParams 键重复: %d" % L})
|
||||
seen[L] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "%s 关卡序号越域(≥1)" % key})
|
||||
var ks := (row as Dictionary).keys()
|
||||
ks.sort()
|
||||
var expect := ROW_KEYS.duplicate()
|
||||
expect.sort()
|
||||
if ks != expect:
|
||||
errs.append({"category": "structural", "message": "%s 字段键集非 ROW_KEYS 闭集" % key})
|
||||
return {"errs": errs, "L": L, "key": key, "ok": false}
|
||||
return {"errs": errs, "L": L, "key": key, "ok": true}
|
||||
|
||||
|
||||
## 行形态域:层深下限(layers_by_level 引用登记——层深递进尾段取数)/分支密度/配比四类合计 1
|
||||
func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var layers: int = int(row.get("layers", 0))
|
||||
if layers < 6:
|
||||
errs.append({"category": "domain", "message": "%s 层深 %d 低于下限 6(层深 L1≥6 已锁)" % [key, layers]})
|
||||
layers_by_level[L] = layers
|
||||
var bd: float = float(row.get("branch_density", -1.0))
|
||||
if bd < 0.0 or bd > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
|
||||
var ratio_sum := 0.0
|
||||
for rk in RATIO_KEYS:
|
||||
var v: float = float(row.get(rk, -1.0))
|
||||
if v < 0.0 or v > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s %s=%f 越域 [0,1]" % [key, rk, v]})
|
||||
ratio_sum += v
|
||||
if not is_equal_approx(ratio_sum, 1.0):
|
||||
errs.append({"category": "domain", "message": "%s 配比四类合计 %f ≠ 1(已锁)" % [key, ratio_sum]})
|
||||
return errs
|
||||
|
||||
|
||||
## 行计数域:入口/出口(L1 恒 1)/战斗保底/岔口去路数锁值
|
||||
func _validate_row_counts(row: Dictionary, L: int, key: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var entry_count: int = int(row.get("entry_count", 0))
|
||||
if entry_count < 1:
|
||||
errs.append({"category": "domain", "message": "%s 入口个数 %d 越域(≥1)" % [key, entry_count]})
|
||||
var exit_count: int = int(row.get("exit_count", 0))
|
||||
if exit_count < 1 or exit_count > 3:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d 越域 [1,3](已锁范围)" % [key, exit_count]})
|
||||
if L == 1 and exit_count != 1:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d ≠ 1(L1 出口恒 1 已锁)" % [key, exit_count]})
|
||||
var bf: int = int(row.get("battle_floor", -1))
|
||||
if bf < 0:
|
||||
errs.append({"category": "domain", "message": "%s 战斗保底 %d 越域(≥0)" % [key, bf]})
|
||||
var fmin: int = int(row.get("fork_ways_min", 0))
|
||||
var fmax: int = int(row.get("fork_ways_max", 0))
|
||||
if fmin != 2 or fmax != 3:
|
||||
errs.append({"category": "domain", "message": "%s 岔口去路数 (%d,%d) ≠ (2,3)(已锁)" % [key, fmin, fmax]})
|
||||
return errs
|
||||
|
||||
|
||||
# 参数档单点读取(键 = level_index,缺行返回空字典——沿 level_row() 体例)
|
||||
func row_by_level_index(level_index: int) -> Dictionary:
|
||||
for row in rows:
|
||||
|
||||
@@ -170,3 +170,8 @@ static func _mark_backward(target: int, outs: Dictionary, seen: Dictionary) -> v
|
||||
if (outs[nid] as Array).has(cur) and not seen.has(int(nid)):
|
||||
seen[int(nid)] = true
|
||||
queue.append(int(nid))
|
||||
|
||||
|
||||
## 序列总关数(AdventureRunner levels_total / MetaProgression 关卡完成累加域的取数口径)
|
||||
func level_count() -> int:
|
||||
return rows.size()
|
||||
@@ -18,7 +18,15 @@ func validate() -> Array:
|
||||
for family in ["boss_keystone", "conversion", "attune_refresh_upgrade"]:
|
||||
if get(family).is_empty():
|
||||
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
|
||||
# boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
|
||||
errs.append_array(_validate_boss_keystone())
|
||||
errs.append_array(_validate_conversion())
|
||||
errs.append_array(_validate_attune_refresh())
|
||||
return errs
|
||||
|
||||
|
||||
## boss_keystone 行族域:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
|
||||
func _validate_boss_keystone() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen_li := {}
|
||||
for row in boss_keystone:
|
||||
var li: int = int(row.get("level_index", 0))
|
||||
@@ -36,9 +44,13 @@ func validate() -> Array:
|
||||
if li_at != i + 1:
|
||||
errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]})
|
||||
break
|
||||
# 无 workshop_items 行族(解锁混合制——条件谓词挂 treasure_items 行内;
|
||||
# 本表不校验秘宝解锁,单点=TreasureItemTable.validate)
|
||||
# conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
|
||||
return errs
|
||||
|
||||
|
||||
## conversion 行族域(属性换算):attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
|
||||
## (无 workshop_items 行族——解锁混合制:条件谓词挂 treasure_items 行内;本表不校验秘宝解锁,单点=TreasureItemTable.validate)
|
||||
func _validate_conversion() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen_conv := {}
|
||||
for row in conversion:
|
||||
var cattr: String = row.get("attr", "")
|
||||
@@ -54,7 +66,12 @@ func validate() -> Array:
|
||||
for conv_key in FORGE_ATTRS:
|
||||
if not seen_conv.has(conv_key):
|
||||
errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key})
|
||||
# attune_refresh_upgrade:level 唯一 + 连续从 1 + charges/cost 域
|
||||
return errs
|
||||
|
||||
|
||||
## attune_refresh_upgrade 行族域(Y 刷新升级):level 唯一 + 连续从 1 + charges/cost 域
|
||||
func _validate_attune_refresh() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen_al := {}
|
||||
for row in attune_refresh_upgrade:
|
||||
var al: int = int(row.get("level", -1))
|
||||
@@ -73,3 +90,21 @@ func validate() -> Array:
|
||||
errs.append({"category": "structural", "message": "attune_refresh_upgrade level 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, al_at]})
|
||||
break
|
||||
return errs
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.crossrun.meta.<m> 直连)----
|
||||
|
||||
## Boss 钥石两档行单点(键 level_index;缺行=空字典)
|
||||
func boss_keystone_row(level_index: int) -> Dictionary:
|
||||
for row in boss_keystone:
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## Y 刷新升级行单点(键 = 等级,缺行返回空字典=未购/已满级合法态)
|
||||
func attune_refresh_row(level: int) -> Dictionary:
|
||||
for row in attune_refresh_upgrade:
|
||||
if int(row.get("level", -1)) == level:
|
||||
return row
|
||||
return {}
|
||||
@@ -39,57 +39,105 @@ func validate() -> Array:
|
||||
errs.append({"category": "dup_key", "message": "node_id 重复: %s" % nid})
|
||||
continue
|
||||
seen[nid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % nid})
|
||||
if not VEINS.has(String(row.get("vein", ""))):
|
||||
errs.append({"category": "enum", "message": "%s vein 出域" % nid})
|
||||
var kind := String(row.get("kind", ""))
|
||||
if not KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s kind 出域" % nid})
|
||||
if not CURRENCIES.has(String(row.get("currency", ""))):
|
||||
errs.append({"category": "enum", "message": "%s currency 出域" % nid})
|
||||
for k in row.keys():
|
||||
if not KEY_WHITELIST.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [nid, String(k)]})
|
||||
var cap := int(row.get("cap", 1))
|
||||
if cap < 1 or cap > 9:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [nid, cap]})
|
||||
if kind in ["unlock", "socket", "fork", "easter"] and cap != 1:
|
||||
errs.append({"category": "cross_field", "message": "%s %s 行 cap 恒 1" % [nid, kind]})
|
||||
var price := int(row.get("price", 0))
|
||||
if price < 0 or price > 9999:
|
||||
errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]})
|
||||
errs.append_array(_validate_row_head(row, nid))
|
||||
errs.append_array(_validate_row_numbers(row, nid))
|
||||
errs.append_array(_validate_row_effects(row, nid))
|
||||
errs.append_array(_validate_row_fork(row, nid, fork_seen))
|
||||
errs.append_array(_validate_requires_dangling(rows, seen))
|
||||
errs.append_array(_validate_requires_level(rows, seen))
|
||||
# 彩蛋 ≤3
|
||||
var easter := 0
|
||||
for row in rows:
|
||||
if String(row.get("kind", "")) == "easter":
|
||||
easter += 1
|
||||
if easter > 3:
|
||||
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(上限 3)" % easter})
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:命名与枚举判据(display_name/vein/kind/currency/行键白名单)
|
||||
func _validate_row_head(row: Dictionary, nid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % nid})
|
||||
if not VEINS.has(String(row.get("vein", ""))):
|
||||
errs.append({"category": "enum", "message": "%s vein 出域" % nid})
|
||||
var kind := String(row.get("kind", ""))
|
||||
if not KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s kind 出域" % nid})
|
||||
if not CURRENCIES.has(String(row.get("currency", ""))):
|
||||
errs.append({"category": "enum", "message": "%s currency 出域" % nid})
|
||||
for k in row.keys():
|
||||
if not KEY_WHITELIST.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [nid, String(k)]})
|
||||
return errs
|
||||
|
||||
|
||||
## 数值域:cap 越域/kind×cap 交叉/price 越域
|
||||
func _validate_row_numbers(row: Dictionary, nid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var kind := String(row.get("kind", ""))
|
||||
var cap := int(row.get("cap", 1))
|
||||
if cap < 1 or cap > 9:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [nid, cap]})
|
||||
if kind in ["unlock", "socket", "fork", "easter"] and cap != 1:
|
||||
errs.append({"category": "cross_field", "message": "%s %s 行 cap 恒 1" % [nid, kind]})
|
||||
var price := int(row.get("price", 0))
|
||||
if price < 0 or price > 9999:
|
||||
errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]})
|
||||
return errs
|
||||
|
||||
|
||||
## 效果域:effects 键词表封闭与局外树层级纪律(archetype 禁入)
|
||||
func _validate_row_effects(row: Dictionary, nid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
|
||||
for ek in fx.keys():
|
||||
if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))):
|
||||
errs.append({"category": "zero_flat", "message": "%s effects 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]})
|
||||
# 🔴 层级纪律:局外树禁规则改写型特性(archetype 键出域=加载致命)
|
||||
if fx.has("archetype"):
|
||||
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——特性禁入局外树)" % nid})
|
||||
return errs
|
||||
|
||||
|
||||
## Fork 结构域:一节点双态结构判据(fork_group 词表/组唯一/主 effects 须空/a/b 双键规则面)
|
||||
func _validate_row_fork(row: Dictionary, nid: String, fork_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var kind := String(row.get("kind", ""))
|
||||
if kind == "fork":
|
||||
var fg := String(row.get("fork_group", ""))
|
||||
if not FORK_GROUPS.has(fg):
|
||||
errs.append({"category": "enum", "message": "%s fork_group 出域" % nid})
|
||||
if fork_seen.has(fg):
|
||||
errs.append({"category": "dup_key", "message": "%s fork 组重复(一组恰一节点): %s" % [nid, fg]})
|
||||
fork_seen[fg] = true
|
||||
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
|
||||
for ek in fx.keys():
|
||||
if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))):
|
||||
errs.append({"category": "zero_flat", "message": "%s effects 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]})
|
||||
# 🔴 层级纪律:局外树禁规则改写型特性(archetype 键出域=加载致命)
|
||||
if fx.has("archetype"):
|
||||
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——特性禁入局外树)" % nid})
|
||||
# Fork 一节点双态结构:fork 行必带 fork_group(每组恰一节点);主 effects 须空,效果只放
|
||||
# a/b 双键且键 ⊆ RULE_KEYS(A/B 只放规则面——层级纪律)
|
||||
if kind == "fork":
|
||||
var fg := String(row.get("fork_group", ""))
|
||||
if not FORK_GROUPS.has(fg):
|
||||
errs.append({"category": "enum", "message": "%s fork_group 出域" % nid})
|
||||
if fork_seen.has(fg):
|
||||
errs.append({"category": "dup_key", "message": "%s fork 组重复(一组恰一节点): %s" % [nid, fg]})
|
||||
fork_seen[fg] = true
|
||||
if not fx.is_empty():
|
||||
errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid})
|
||||
for skey in ["effects_a", "effects_b"]:
|
||||
var sd: Dictionary[Variant, Variant] = row.get(skey, {}) as Dictionary
|
||||
if sd.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]})
|
||||
for k in sd.keys():
|
||||
if not RULE_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s %s 键出域(A/B 只放规则面): %s" % [nid, skey, String(k)]})
|
||||
# requires 悬空引用
|
||||
if not fx.is_empty():
|
||||
errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid})
|
||||
for skey in ["effects_a", "effects_b"]:
|
||||
var sd: Dictionary[Variant, Variant] = row.get(skey, {}) as Dictionary
|
||||
if sd.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]})
|
||||
for k in sd.keys():
|
||||
if not RULE_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s %s 键出域(A/B 只放规则面): %s" % [nid, skey, String(k)]})
|
||||
return errs
|
||||
|
||||
|
||||
## requires 悬空引用域
|
||||
func _validate_requires_dangling(rows: Array, seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
for row in rows:
|
||||
for req in (row.get("requires", []) as Array):
|
||||
if not seen.has(String(req)):
|
||||
errs.append({"category": "dangling", "message": "%s requires 悬空: %s" % [String(row.get("node_id", "")), String(req)]})
|
||||
# requires_level 等级阈值前置(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
|
||||
return errs
|
||||
|
||||
|
||||
## requires_level 等级阈值前置域(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
|
||||
func _validate_requires_level(rows: Array, seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
for row in rows:
|
||||
var nid2: String = String(row.get("node_id", ""))
|
||||
var rl_v: Variant = row.get("requires_level", {})
|
||||
@@ -101,13 +149,6 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "%s requires_level 阈值须 ≥1: %s=%d" % [nid2, String(req_id), int(rl_v[req_id])]})
|
||||
elif row.has("requires_level"):
|
||||
errs.append({"category": "domain", "message": "%s requires_level 须为 Dictionary {node_id: n}" % nid2})
|
||||
# 彩蛋 ≤3
|
||||
var easter := 0
|
||||
for row in rows:
|
||||
if String(row.get("kind", "")) == "easter":
|
||||
easter += 1
|
||||
if easter > 3:
|
||||
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(上限 3)" % easter})
|
||||
return errs
|
||||
|
||||
|
||||
|
||||
@@ -30,84 +30,129 @@ func validate() -> Array:
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "reward_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var ctype: String = row.get("cell_type", "")
|
||||
if not CELL_TYPES.has(ctype):
|
||||
errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]})
|
||||
var pol: String = row.get("polarity", "")
|
||||
if not POLARITIES.has(pol):
|
||||
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
|
||||
var atk_type: String = String(row.get("attack_type", ""))
|
||||
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
|
||||
# aux 投放行专属校验:极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
|
||||
# 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable)
|
||||
if ctype == TREASURE_ITEM_TYPE:
|
||||
# 负向行 = 危险格极性字典占位(雷安同池摇号下危险格可摇中 aux 身份——负路由零兑付兜底,
|
||||
# effect_id 空/power 0/weight 0 不入池)
|
||||
if pol == "negative":
|
||||
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
|
||||
continue
|
||||
if String(row.get("effect_id", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
|
||||
if int(row.get("power", 0)) != 0 or row.has("power_min"):
|
||||
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 TreasureItemTable)" % rid})
|
||||
var pool: String = row.get("pool_tag", "")
|
||||
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
|
||||
if not POOL_TAGS.has(pool) and not pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
|
||||
elif pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
var power: int = int(row.get("power", 0))
|
||||
var weight: int = int(row.get("weight", -1))
|
||||
var tier: int = int(row.get("tier", 0))
|
||||
if weight < 0 or weight > 999:
|
||||
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
|
||||
if tier < 1 or tier > 8:
|
||||
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
|
||||
var stacks: int = int(row.get("stacks", 1))
|
||||
if stacks < 1 or stacks > 16:
|
||||
errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]})
|
||||
# 跨字段:effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。
|
||||
# flat 行 power ≥ 1(直付/直兑行:治疗/群伤/直伤/行动点)——action 型 = 排队点数行(power = ±点数量级)。
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if power < 0 or power > 999:
|
||||
errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]})
|
||||
if eff == "" and power < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid})
|
||||
# 区间配对(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚
|
||||
var has_min: bool = row.has("power_min")
|
||||
var has_max: bool = row.has("power_max")
|
||||
if has_min != has_max:
|
||||
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
|
||||
elif has_min:
|
||||
var pmin: int = int(row.get("power_min", 0))
|
||||
var pmax: int = int(row.get("power_max", 0))
|
||||
if pmin < 1 or pmax > 999 or pmin > pmax:
|
||||
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
|
||||
if eff != "":
|
||||
errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid})
|
||||
if power != int((pmin + pmax) / 2.0):
|
||||
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
|
||||
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
|
||||
errs.append_array(_validate_row_axes(row, rid))
|
||||
var aux_out := _validate_row_aux(row, rid)
|
||||
errs.append_array(aux_out["errs"])
|
||||
if aux_out["skip"]:
|
||||
continue
|
||||
errs.append_array(_validate_row_pool_tag(row, rid))
|
||||
errs.append_array(_validate_row_values(row, rid))
|
||||
errs.append_array(_validate_row_power_range(row, rid))
|
||||
# 字典完整性 / 池护栏累计(cell 池行)
|
||||
if pool == "cell" and CELL_TYPES.has(ctype) and POLARITIES.has(pol):
|
||||
cell_dict["%s|%s" % [ctype, pol]] = true
|
||||
if pol == "positive" and weight > 0:
|
||||
if String(row.get("pool_tag", "")) == "cell" and CELL_TYPES.has(String(row.get("cell_type", ""))) and POLARITIES.has(String(row.get("polarity", ""))):
|
||||
cell_dict["%s|%s" % [String(row.get("cell_type", "")), String(row.get("polarity", ""))]] = true
|
||||
if String(row.get("polarity", "")) == "positive" and int(row.get("weight", -1)) > 0:
|
||||
cell_positive_live += 1
|
||||
if pool == "clear_default" and eff == "" and power >= 1:
|
||||
if String(row.get("pool_tag", "")) == "clear_default" and String(row.get("effect_id", "")) == "" and int(row.get("power", 0)) >= 1:
|
||||
clear_value_ok = true
|
||||
# ① 字典完整性:五型 × 正负两行齐备(缺任一即致命)
|
||||
errs.append_array(_validate_table_closure(cell_dict, cell_positive_live, clear_value_ok))
|
||||
return errs
|
||||
|
||||
|
||||
## 轴域:cell_type 五型封闭/polarity 两极/attack_type 可省略枚举
|
||||
func _validate_row_axes(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var ctype: String = row.get("cell_type", "")
|
||||
if not CELL_TYPES.has(ctype):
|
||||
errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]})
|
||||
var pol: String = row.get("polarity", "")
|
||||
if not POLARITIES.has(pol):
|
||||
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
|
||||
var atk_type: String = String(row.get("attack_type", ""))
|
||||
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
|
||||
return errs
|
||||
|
||||
|
||||
## aux 投放行域(掉落投放行):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
|
||||
## 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable);
|
||||
## 负占位行返回 skip=true(跳过行剩余判据——原 continue 语义)
|
||||
func _validate_row_aux(row: Dictionary, rid: String) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skip := false
|
||||
if String(row.get("cell_type", "")) == TREASURE_ITEM_TYPE:
|
||||
# 负向行 = 危险格极性字典占位(雷安同池摇号下危险格可摇中 aux 身份——负路由零兑付兜底,
|
||||
# effect_id 空/power 0/weight 0 不入池)
|
||||
if String(row.get("polarity", "")) == "negative":
|
||||
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
|
||||
skip = true
|
||||
return {"errs": errs, "skip": skip}
|
||||
if String(row.get("effect_id", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
|
||||
if int(row.get("power", 0)) != 0 or row.has("power_min"):
|
||||
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 TreasureItemTable)" % rid})
|
||||
return {"errs": errs, "skip": skip}
|
||||
|
||||
|
||||
## 池标签域:pool_tag 词表与 clear_<biome> 命名空间预留拒绝/display_name 非空
|
||||
func _validate_row_pool_tag(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var pool: String = row.get("pool_tag", "")
|
||||
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
|
||||
if not POOL_TAGS.has(pool) and not pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
|
||||
elif pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
return errs
|
||||
|
||||
|
||||
## 数值域:weight/tier/stacks/power 越域与 flat 行 power ≥ 1
|
||||
## (effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。
|
||||
## flat 行 power ≥ 1(直付/直兑行:治疗/群伤/直伤/行动点)——action 型 = 排队点数行(power = ±点数量级))
|
||||
func _validate_row_values(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var power: int = int(row.get("power", 0))
|
||||
var weight: int = int(row.get("weight", -1))
|
||||
var tier: int = int(row.get("tier", 0))
|
||||
if weight < 0 or weight > 999:
|
||||
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
|
||||
if tier < 1 or tier > 8:
|
||||
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
|
||||
var stacks: int = int(row.get("stacks", 1))
|
||||
if stacks < 1 or stacks > 16:
|
||||
errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]})
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if power < 0 or power > 999:
|
||||
errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]})
|
||||
if eff == "" and power < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid})
|
||||
return errs
|
||||
|
||||
|
||||
## 区间配对域(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚
|
||||
func _validate_row_power_range(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var power: int = int(row.get("power", 0))
|
||||
var eff: String = row.get("effect_id", "")
|
||||
var has_min: bool = row.has("power_min")
|
||||
var has_max: bool = row.has("power_max")
|
||||
if has_min != has_max:
|
||||
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
|
||||
elif has_min:
|
||||
var pmin: int = int(row.get("power_min", 0))
|
||||
var pmax: int = int(row.get("power_max", 0))
|
||||
if pmin < 1 or pmax > 999 or pmin > pmax:
|
||||
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
|
||||
if eff != "":
|
||||
errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid})
|
||||
if power != int((pmin + pmax) / 2.0):
|
||||
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
|
||||
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
|
||||
return errs
|
||||
|
||||
|
||||
## 表闭合域(三尾段):①九型字典完整性(类型×极性齐备)②池空护栏 ③clear_default 恒正当量
|
||||
func _validate_table_closure(cell_dict: Dictionary, cell_positive_live: int, clear_value_ok: bool) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
for ctype in CELL_TYPE_KEYS:
|
||||
for pol in POLARITIES:
|
||||
if not cell_dict.has("%s|%s" % [ctype, pol]):
|
||||
errs.append({"category": "cross_field", "message": "九型字典缺行: %s %s——(类型, 极性) 齐备为加载前提" % [ctype, pol]})
|
||||
# ② 池空护栏:cell 池 ≥1 正向行 weight>0(摇号池非空)
|
||||
if cell_positive_live < 1:
|
||||
errs.append({"category": "cross_field", "message": "cell 池零活正向行(weight>0)——摇号池空"})
|
||||
# ③ clear_default 恒正当量(行动经济 条款④——清图奖励 > 0 的行级兑现口径)
|
||||
if not clear_value_ok:
|
||||
errs.append({"category": "cross_field", "message": "clear_default 池无正当量活行(清图奖励 > 0 违例)"})
|
||||
return errs
|
||||
|
||||
@@ -133,3 +133,55 @@ func _validate_price_factors(errs: Array[Dictionary]) -> void:
|
||||
errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price})
|
||||
if spend_discount <= 0.0 or spend_discount > 1.0:
|
||||
errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount})
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.run.shop.<m> 直连)----
|
||||
|
||||
## GP 掉落行单点(键 tier;缺行=空字典——掉落按 0 兑付的兜底在调用方)
|
||||
func gp_drop_row(tier: String) -> Dictionary:
|
||||
for row in gp_drops:
|
||||
if String(row.get("tier", "")) == tier:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 关卡 GP 加成单点(键 level_index;缺行=0)
|
||||
func level_gp_bonus_at(level_index: int) -> int:
|
||||
for row in level_gp_bonus:
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return int(row.get("bonus", 0))
|
||||
return 0
|
||||
|
||||
|
||||
## 价格关卡加成单点(键 level_index;缺行=0)
|
||||
func price_level_bonus_at(level_index: int) -> int:
|
||||
for row in price_level_bonus:
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return int(row.get("bonus", 0))
|
||||
return 0
|
||||
|
||||
|
||||
## 商品行族({product_id, display_name, attr, flat_value, base_price, price_increment})
|
||||
func product_rows() -> Array:
|
||||
return products
|
||||
|
||||
|
||||
## 商品行单点(键 product_id;缺行=空字典)
|
||||
func product_row(product_id: String) -> Dictionary:
|
||||
for row in products:
|
||||
if String(row.get("product_id", "")) == product_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
|
||||
func pool_rows() -> Array:
|
||||
return pool_shelf
|
||||
|
||||
|
||||
## 货架行单点(键 kind_id;缺行=空字典)
|
||||
func pool_row(kind_id: String) -> Dictionary:
|
||||
for row in pool_shelf:
|
||||
if String(row.get("kind_id", "")) == kind_id:
|
||||
return row
|
||||
return {}
|
||||
@@ -31,73 +31,108 @@ func validate() -> Array:
|
||||
var seen := {}
|
||||
var has_basic := false
|
||||
for row in rows:
|
||||
var sid: String = row.get("skill_id", "")
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
|
||||
var head := _validate_row_head(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
if seen.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
|
||||
seen[sid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
|
||||
var type: String = row.get("type", "")
|
||||
if not SKILL_TYPES.has(type):
|
||||
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
|
||||
continue
|
||||
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
|
||||
if type == "basic" and row.has("trigger"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
|
||||
# 冷却域(全类型):施放后连续 N 拍不可摇中
|
||||
var cd: int = int(row.get("cooldown_beats", -1))
|
||||
if cd < 0 or cd > 99:
|
||||
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
|
||||
# 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2)
|
||||
var sid: String = head["sid"]
|
||||
var type: String = head["type"]
|
||||
_validate_common_fields(sid, row, type, errs)
|
||||
if type == "basic":
|
||||
has_basic = true
|
||||
_validate_basic_row(sid, row, cd, errs)
|
||||
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
|
||||
# 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
|
||||
_validate_basic_row(sid, row, int(row.get("cooldown_beats", -1)), errs)
|
||||
if type == "damage":
|
||||
if not row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
|
||||
else:
|
||||
var m: float = float(row.damage_mult)
|
||||
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
|
||||
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
|
||||
if row.has("rider_mult"):
|
||||
var rm: float = float(row.rider_mult)
|
||||
if rm <= 0.0 or rm > RIDER_MULT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
|
||||
# 增益技:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
|
||||
_validate_damage_row(sid, row, errs)
|
||||
if type == "buff":
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
|
||||
var bd: int = int(row.get("buff_duration", 0))
|
||||
if bd < 1 or bd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
|
||||
# 妨害技:0 伤害施放动作,对主角施加负面状态——
|
||||
# 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
|
||||
_validate_buff_row(sid, row, errs)
|
||||
if type == "debuff":
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
|
||||
var dbd: int = int(row.get("buff_duration", 0))
|
||||
if dbd < 1 or dbd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
|
||||
_validate_debuff_row(sid, row, errs)
|
||||
_validate_table_invariants(has_basic, errs)
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:skill_id 非空与查重/display_name/type 枚举(ok=跳过行剩余判据)
|
||||
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var sid: String = row.get("skill_id", "")
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
|
||||
return {"errs": errs, "sid": sid, "type": "", "ok": false}
|
||||
if seen.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
|
||||
seen[sid] = true
|
||||
return {"errs": errs, "sid": sid, "type": "", "ok": false}
|
||||
seen[sid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
|
||||
var type: String = row.get("type", "")
|
||||
if not SKILL_TYPES.has(type):
|
||||
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
|
||||
return {"errs": errs, "sid": sid, "type": type, "ok": false}
|
||||
return {"errs": errs, "sid": sid, "type": type, "ok": true}
|
||||
|
||||
|
||||
## 公共域(全类型):trigger 触发条件校验/普攻行禁配 trigger/冷却域
|
||||
func _validate_common_fields(sid: String, row: Dictionary, type: String, errs: Array[Dictionary]) -> void:
|
||||
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
|
||||
if type == "basic" and row.has("trigger"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
|
||||
# 冷却域(全类型):施放后连续 N 拍不可摇中
|
||||
var cd: int = int(row.get("cooldown_beats", -1))
|
||||
if cd < 0 or cd > 99:
|
||||
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
|
||||
|
||||
|
||||
## 伤害技域:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
|
||||
## 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
|
||||
func _validate_damage_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if not row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
|
||||
else:
|
||||
var m: float = float(row.damage_mult)
|
||||
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
|
||||
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
|
||||
if row.has("rider_mult"):
|
||||
var rm: float = float(row.rider_mult)
|
||||
if rm <= 0.0 or rm > RIDER_MULT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
|
||||
|
||||
|
||||
## 增益技域:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
|
||||
func _validate_buff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
|
||||
var bd: int = int(row.get("buff_duration", 0))
|
||||
if bd < 1 or bd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
|
||||
|
||||
|
||||
## 妨害技域:0 伤害施放动作,对主角施加负面状态——
|
||||
## 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
|
||||
func _validate_debuff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
|
||||
var dbd: int = int(row.get("buff_duration", 0))
|
||||
if dbd < 1 or dbd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
|
||||
|
||||
|
||||
## 表不变量域:普攻行存在性(池构成纪律的结构前提)与保底旋钮值域
|
||||
func _validate_table_invariants(has_basic: bool, errs: Array[Dictionary]) -> void:
|
||||
if not has_basic:
|
||||
errs.append({"category": "structural", "message": "SkillTable 无普攻行(basic)——池构成纪律的结构前提"})
|
||||
if pity_threshold < 1 or pity_threshold > 99:
|
||||
errs.append({"category": "domain", "message": "pity_threshold 越域 [1,99]: %d" % pity_threshold})
|
||||
return errs
|
||||
|
||||
|
||||
# trigger 域校验(敌人系统 v1.2 触发条件:四键白名单/词表出域/类型分派——枚举事实算子限域 eq/neq
|
||||
|
||||
@@ -18,30 +18,12 @@ func validate() -> Array:
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var eid: String = row.get("effect_id", "")
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
|
||||
var head := _validate_row_head(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
if seen.has(eid):
|
||||
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
|
||||
seen[eid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
if not TARGET_SIDES.has(row.get("target_side", "")):
|
||||
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
|
||||
var unit: String = row.get("unit", "")
|
||||
if not UNITS.has(unit):
|
||||
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
|
||||
if not STACK_POLICIES.has(row.get("stack_policy", "")):
|
||||
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
|
||||
var m: int = int(row.get("magnitude", 0))
|
||||
# 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1 / ac ∈ [-20,20] 且 ≠0(AC 阈值轴——幅度域独立,防呆上限 20)
|
||||
if unit == "percent" and (m == 0 or m < -100 or m > 100):
|
||||
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
|
||||
if unit == "flat" and m < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
|
||||
if unit == "ac" and (m == 0 or m < -20 or m > 20):
|
||||
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
|
||||
var eid: String = head["eid"]
|
||||
errs.append_array(_validate_magnitude_domain(row, eid, head["unit"]))
|
||||
var dur: int = int(row.get("duration_settlements", 0))
|
||||
if dur < 1 or dur > 99:
|
||||
errs.append({"category": "domain", "message": "%s duration_settlements 越域 [1,99]: %d" % [eid, dur]})
|
||||
@@ -51,6 +33,44 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:effect_id 非空与查重/display_name/target_side/unit/stack_policy 词表
|
||||
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var eid: String = row.get("effect_id", "")
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
|
||||
return {"errs": errs, "eid": eid, "unit": "", "ok": false}
|
||||
if seen.has(eid):
|
||||
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
|
||||
seen[eid] = true
|
||||
return {"errs": errs, "eid": eid, "unit": "", "ok": false}
|
||||
seen[eid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
if not TARGET_SIDES.has(row.get("target_side", "")):
|
||||
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
|
||||
var unit: String = row.get("unit", "")
|
||||
if not UNITS.has(unit):
|
||||
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
|
||||
if not STACK_POLICIES.has(row.get("stack_policy", "")):
|
||||
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
|
||||
return {"errs": errs, "eid": eid, "unit": unit, "ok": true}
|
||||
|
||||
|
||||
## 幅度量纲域(三段量纲判据——percent/flat/ac 各自值域;
|
||||
## ac = AC 阈值轴效果,非池百分比非定点值——幅度域独立,防呆上限 20)
|
||||
func _validate_magnitude_domain(row: Dictionary, eid: String, unit: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var m: int = int(row.get("magnitude", 0))
|
||||
if unit == "percent" and (m == 0 or m < -100 or m > 100):
|
||||
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
|
||||
if unit == "flat" and m < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
|
||||
if unit == "ac" and (m == 0 or m < -20 or m > 20):
|
||||
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
|
||||
return errs
|
||||
|
||||
|
||||
func row_by_id(effect_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if row.get("effect_id", "") == effect_id:
|
||||
|
||||
@@ -73,136 +73,195 @@ func validate() -> Array:
|
||||
errs.append({"category": "dup_key", "message": "item_id 重复: %s" % iid})
|
||||
continue
|
||||
seen[iid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
|
||||
var retired: bool = bool(row.get("retired", false))
|
||||
var deferred: bool = bool(row.get("deferred", false))
|
||||
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
|
||||
var tags: Array[Variant] = row.get("dimension_tags", [])
|
||||
if tags.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
|
||||
for tag in tags:
|
||||
if not DIMENSIONS.has(String(tag)):
|
||||
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
|
||||
var kinds: Array[Variant] = row.get("effect_kinds", [])
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if not TARGETS.has(target):
|
||||
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
|
||||
var phase: String = String(row.get("use_phase", "active_dig"))
|
||||
if not USE_PHASES.has(phase):
|
||||
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
|
||||
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
|
||||
for k in row.keys():
|
||||
if not KEY_WHITELIST.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
|
||||
# triggers 事件订阅行(被动件形态;与 effect_kinds 互斥校验
|
||||
# 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
|
||||
# (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
|
||||
if row.has("triggers"):
|
||||
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
|
||||
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
|
||||
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
|
||||
var form: String = String(row.get("form", ""))
|
||||
if not FORMS.has(form):
|
||||
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
|
||||
if retired or deferred:
|
||||
errs.append_array(_validate_row_head(row, iid))
|
||||
if bool(row.get("retired", false)) or bool(row.get("deferred", false)):
|
||||
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
|
||||
continue
|
||||
# ---- 活行校验 ----
|
||||
if kinds.is_empty() and not row.has("triggers"):
|
||||
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
|
||||
var kind_seen := {}
|
||||
for k in kinds:
|
||||
var kind: String = String(k)
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
|
||||
if kind_seen.has(kind):
|
||||
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
|
||||
kind_seen[kind] = true
|
||||
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
|
||||
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
|
||||
for rv in REVEAL_KINDS:
|
||||
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
|
||||
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
|
||||
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
|
||||
if target == "board_wide" and kind_seen.has("mine_reveal"):
|
||||
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
|
||||
# magnitude:MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
|
||||
var mag: int = int(row.get("magnitude", 0))
|
||||
if mag < 0 or mag > 999:
|
||||
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
|
||||
var mag_consuming := false
|
||||
for k in kind_seen.keys():
|
||||
if MAGNITUDE_KINDS.has(String(k)):
|
||||
mag_consuming = true
|
||||
if mag_consuming and mag < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
|
||||
if not mag_consuming and mag != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
|
||||
# ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)----
|
||||
errs.append_array(_validate_row_active(row, iid, kind_seen))
|
||||
var form: String = String(row.get("form", ""))
|
||||
if form != "":
|
||||
if int(row.get("slot_cost", 1)) != 1:
|
||||
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
|
||||
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
|
||||
var tier: String = String(row.get("tier", ""))
|
||||
if not TIERS.has(tier):
|
||||
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
|
||||
var charges: int = int(row.get("charges_per_adventure", 0))
|
||||
if form == "passive" and charges != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges < 0:
|
||||
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges == 0:
|
||||
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
|
||||
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
|
||||
var has_gate := kind_seen.has("ap_charge_safe")
|
||||
if not has_gate and row.has("triggers"):
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
|
||||
has_gate = true
|
||||
break
|
||||
if not has_gate:
|
||||
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
|
||||
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
|
||||
var creset: String = String(row.get("charge_reset", "adventure"))
|
||||
if not CHARGE_RESETS.has(creset):
|
||||
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
|
||||
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
|
||||
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
|
||||
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
|
||||
var price: int = int(row.get("unlock_keystone_cost", 0))
|
||||
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
|
||||
if price != 0 and ureq_count > 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
|
||||
if price < 0 or price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
|
||||
var eff_rounds: int = int(row.get("effective_rounds", 0))
|
||||
if eff_rounds < 0 or eff_rounds > 999:
|
||||
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
|
||||
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
|
||||
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
|
||||
var charge_n: int = int(row.get("charge_n", 0))
|
||||
if charge_n < 0 or charge_n > 999:
|
||||
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
|
||||
if kind_seen.has("ap_charge_safe") and charge_n < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
|
||||
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
|
||||
# unlock_requires 谓词行:空 = 已解锁(首批全解锁合法态,底稿 §2.2);非空键 ∈ 计数键封闭枚举
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", [])
|
||||
var ukeys := {}
|
||||
for req in ureqs:
|
||||
var ukey: String = String(req.get("key", ""))
|
||||
if not UNLOCK_COUNTER_KEYS.has(ukey):
|
||||
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
|
||||
if ukeys.has(ukey):
|
||||
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
|
||||
ukeys[ukey] = true
|
||||
var uth: int = int(req.get("threshold", 0))
|
||||
if uth < 1 or uth > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
|
||||
errs.append_array(_validate_row_attunement(row, iid, kind_seen))
|
||||
errs.append_array(_validate_row_unlock_costs(row, iid))
|
||||
errs.append_array(_validate_row_effect_numbers(row, iid, kind_seen))
|
||||
errs.append_array(_validate_row_unlock_requires(row, iid))
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:display_name/维度标签/target/use_phase/行键白名单/triggers 订阅形态/form 词表
|
||||
func _validate_row_head(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
|
||||
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
|
||||
var tags: Array[Variant] = row.get("dimension_tags", [])
|
||||
if tags.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
|
||||
for tag in tags:
|
||||
if not DIMENSIONS.has(String(tag)):
|
||||
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if not TARGETS.has(target):
|
||||
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
|
||||
var phase: String = String(row.get("use_phase", "active_dig"))
|
||||
if not USE_PHASES.has(phase):
|
||||
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
|
||||
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
|
||||
for k in row.keys():
|
||||
if not KEY_WHITELIST.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
|
||||
errs.append_array(_validate_row_triggers(row, iid))
|
||||
var form: String = String(row.get("form", ""))
|
||||
if not FORMS.has(form):
|
||||
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
|
||||
return errs
|
||||
|
||||
|
||||
## 订阅形态域:triggers 事件订阅行判据与行级触发词表校验(被动件形态;与 effect_kinds 互斥校验
|
||||
## 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
|
||||
## (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
|
||||
func _validate_row_triggers(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if row.has("triggers"):
|
||||
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
|
||||
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
|
||||
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
|
||||
return errs
|
||||
|
||||
|
||||
## 活行域:effect_kinds 非空与词表/真视窄域与 board_wide 承重护栏/magnitude 语义(kind_seen 引用填充——同调域消费)
|
||||
func _validate_row_active(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var kinds: Array[Variant] = row.get("effect_kinds", [])
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if kinds.is_empty() and not row.has("triggers"):
|
||||
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
|
||||
for k in kinds:
|
||||
var kind: String = String(k)
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
|
||||
if kind_seen.has(kind):
|
||||
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
|
||||
kind_seen[kind] = true
|
||||
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
|
||||
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
|
||||
for rv in REVEAL_KINDS:
|
||||
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
|
||||
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
|
||||
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
|
||||
if target == "board_wide" and kind_seen.has("mine_reveal"):
|
||||
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
|
||||
errs.append_array(_validate_row_magnitude(row, iid, kind_seen))
|
||||
return errs
|
||||
|
||||
|
||||
## N 值语义域:magnitude 越域与 MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
|
||||
func _validate_row_magnitude(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var mag: int = int(row.get("magnitude", 0))
|
||||
if mag < 0 or mag > 999:
|
||||
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
|
||||
var mag_consuming := false
|
||||
for k in kind_seen.keys():
|
||||
if MAGNITUDE_KINDS.has(String(k)):
|
||||
mag_consuming = true
|
||||
if mag_consuming and mag < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
|
||||
if not mag_consuming and mag != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
|
||||
return errs
|
||||
|
||||
|
||||
## 同调充能与档位域(form 非空——装配面;过渡行 form="" 不校验):槽位统一/档位必填/充能值域与积点制受理闸/充能重置周期
|
||||
func _validate_row_attunement(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if int(row.get("slot_cost", 1)) != 1:
|
||||
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
|
||||
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
|
||||
var tier: String = String(row.get("tier", ""))
|
||||
if not TIERS.has(tier):
|
||||
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
|
||||
var charges: int = int(row.get("charges_per_adventure", 0))
|
||||
var form: String = String(row.get("form", ""))
|
||||
if form == "passive" and charges != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges < 0:
|
||||
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges == 0:
|
||||
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
|
||||
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
|
||||
errs.append_array(_validate_row_charge_gate(row, iid, kind_seen))
|
||||
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
|
||||
var creset: String = String(row.get("charge_reset", "adventure"))
|
||||
if not CHARGE_RESETS.has(creset):
|
||||
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
|
||||
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
|
||||
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
|
||||
return errs
|
||||
|
||||
|
||||
## 积点制受理闸域:charges=0 的 active_charged 行必须带使用闸(ap_charge_safe 滑耗或 counter_spend 受理闸)
|
||||
func _validate_row_charge_gate(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var has_gate := kind_seen.has("ap_charge_safe")
|
||||
if not has_gate and row.has("triggers"):
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
|
||||
has_gate = true
|
||||
break
|
||||
if not has_gate:
|
||||
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
|
||||
return errs
|
||||
|
||||
|
||||
## 解锁互斥域(form 非空行,契约 6):钥石购买价与条件谓词互斥——每件单解锁方式,混合不做
|
||||
func _validate_row_unlock_costs(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
|
||||
var price: int = int(row.get("unlock_keystone_cost", 0))
|
||||
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
|
||||
if price != 0 and ureq_count > 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
|
||||
if price < 0 or price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
|
||||
return errs
|
||||
|
||||
|
||||
## 效果数值域(form 非空行):effective_rounds 与 charge_n 的值域及 kind 交叉
|
||||
func _validate_row_effect_numbers(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var eff_rounds: int = int(row.get("effective_rounds", 0))
|
||||
if eff_rounds < 0 or eff_rounds > 999:
|
||||
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
|
||||
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
|
||||
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
|
||||
var charge_n: int = int(row.get("charge_n", 0))
|
||||
if charge_n < 0 or charge_n > 999:
|
||||
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
|
||||
if kind_seen.has("ap_charge_safe") and charge_n < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
|
||||
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
|
||||
return errs
|
||||
|
||||
|
||||
## 谓词行域:unlock_requires 计数键封闭枚举与阈值(空 = 已解锁——首批全解锁合法态,底稿 §2.2)
|
||||
func _validate_row_unlock_requires(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", [])
|
||||
var ukeys := {}
|
||||
for req in ureqs:
|
||||
var ukey: String = String(req.get("key", ""))
|
||||
if not UNLOCK_COUNTER_KEYS.has(ukey):
|
||||
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
|
||||
if ukeys.has(ukey):
|
||||
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
|
||||
ukeys[ukey] = true
|
||||
var uth: int = int(req.get("threshold", 0))
|
||||
if uth < 1 or uth > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
|
||||
return errs
|
||||
|
||||
|
||||
|
||||
@@ -26,57 +26,82 @@ func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var vid: String = row.get("variant_id", "")
|
||||
if vid == "":
|
||||
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
|
||||
var head := _validate_row(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
if seen.has(vid):
|
||||
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
|
||||
var vid: String = head["vid"]
|
||||
errs.append_array(_validate_effect_interlocks(row, vid))
|
||||
errs.append_array(_validate_global_interlocks(row, vid))
|
||||
return errs
|
||||
|
||||
|
||||
## 行域:variant_id 非空与查重/display_name/unlock_level/四词表(scope/effect/trigger/placement)
|
||||
func _validate_row(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var vid: String = row.get("variant_id", "")
|
||||
if vid == "":
|
||||
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
|
||||
return {"errs": errs, "vid": vid, "ok": false}
|
||||
if seen.has(vid):
|
||||
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
|
||||
seen[vid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
|
||||
var ul: int = int(row.get("unlock_level", 0))
|
||||
if ul < 1 or ul > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
|
||||
if not SCOPES.has(row.get("scope", "")):
|
||||
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
|
||||
if not EFFECTS.has(row.get("effect", "")):
|
||||
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
|
||||
if not TRIGGERS.has(row.get("trigger", "")):
|
||||
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
|
||||
if not PLACEMENTS.has(row.get("placement", "")):
|
||||
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
|
||||
# 跨字段互锁(加载期致命——枚举语义一致性立闸):
|
||||
# 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数
|
||||
if row.get("effect", "") == "flat_damage":
|
||||
if row.get("trigger", "") != "on_open":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
|
||||
if row.get("placement", "") != "closed_only":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
|
||||
var ev: int = int(row.get("effect_value", 0))
|
||||
if ev < 1 or ev > 9:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
|
||||
# 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
|
||||
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
|
||||
# 信息遮蔽型只允许落安全格(遮蔽对象是数字;危险格无数字可遮)——placement=any 时由读取方
|
||||
# 呈现面按 kind=safe 自然短路,此处不设布置约束
|
||||
# 全局作用域型互锁(resource_face ⟺ scope=global 三条 + 新值域;
|
||||
# 判定键=effect/scope 词值,既有六 region 行(flat_damage/hide_number)不入本组——零误伤)
|
||||
if row.get("effect", "") == "resource_face":
|
||||
if row.get("scope", "") != "global":
|
||||
errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid})
|
||||
# resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用)
|
||||
var gev: int = int(row.get("effect_value", 0))
|
||||
if gev < 0 or gev > 50:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]})
|
||||
if row.get("scope", "") == "global":
|
||||
if row.get("effect", "") != "resource_face":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid})
|
||||
if row.get("placement", "") != "any":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid})
|
||||
if row.get("trigger", "") != "always":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(整图恒在,无触发时机)" % vid})
|
||||
return {"errs": errs, "vid": vid, "ok": false}
|
||||
seen[vid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
|
||||
var ul: int = int(row.get("unlock_level", 0))
|
||||
if ul < 1 or ul > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
|
||||
if not SCOPES.has(row.get("scope", "")):
|
||||
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
|
||||
if not EFFECTS.has(row.get("effect", "")):
|
||||
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
|
||||
if not TRIGGERS.has(row.get("trigger", "")):
|
||||
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
|
||||
if not PLACEMENTS.has(row.get("placement", "")):
|
||||
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
|
||||
return {"errs": errs, "vid": vid, "ok": true}
|
||||
|
||||
|
||||
## 效果互锁域(加载期致命——枚举语义一致性立闸):
|
||||
## 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数;
|
||||
## 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
|
||||
## (信息遮蔽型只允许落安全格——遮蔽对象是数字,危险格无数字可遮;placement=any 时由读取方
|
||||
## 呈现面按 kind=safe 自然短路,此处不设布置约束)
|
||||
func _validate_effect_interlocks(row: Dictionary, vid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if row.get("effect", "") == "flat_damage":
|
||||
if row.get("trigger", "") != "on_open":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
|
||||
if row.get("placement", "") != "closed_only":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
|
||||
var ev: int = int(row.get("effect_value", 0))
|
||||
if ev < 1 or ev > 9:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
|
||||
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
|
||||
return errs
|
||||
|
||||
|
||||
## 全局作用域互锁域(resource_face ⟺ scope=global;判定键=effect/scope 词值,
|
||||
## 既有六 region 行(flat_damage/hide_number)不入本组——零误伤);
|
||||
## resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用)
|
||||
func _validate_global_interlocks(row: Dictionary, vid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if row.get("effect", "") == "resource_face":
|
||||
if row.get("scope", "") != "global":
|
||||
errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid})
|
||||
var gev: int = int(row.get("effect_value", 0))
|
||||
if gev < 0 or gev > 50:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]})
|
||||
if row.get("scope", "") == "global":
|
||||
if row.get("effect", "") != "resource_face":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid})
|
||||
if row.get("placement", "") != "any":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid})
|
||||
if row.get("trigger", "") != "always":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(整图恒在,无触发时机)" % vid})
|
||||
return errs
|
||||
|
||||
|
||||
@@ -85,3 +110,38 @@ func row_by_id(variant_id: String) -> Dictionary:
|
||||
if row.get("variant_id", "") == variant_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# ---- 候选池访问器(自 ConfigDB 归位;消费面 db.combat.variation.<m> 直连)----
|
||||
# 门控第三维 = 节点身份(全局作用域变异):scope=global 行仅 boss/elite 节点入池,
|
||||
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
|
||||
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
|
||||
|
||||
## 候选池门控(∅ board_types = 通配;字段名对齐 board_type 词表,无 biome_tags)
|
||||
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
|
||||
var pool: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if String(row.get("scope", "region")) == "global" \
|
||||
and node_type != "boss" and node_type != "elite":
|
||||
continue
|
||||
if int(row.get("unlock_level", 999)) > level_index:
|
||||
continue
|
||||
var tags: Array[Variant] = row.get("board_types", [])
|
||||
if tags.is_empty():
|
||||
pool.append(row)
|
||||
continue
|
||||
if tags.has(board_type):
|
||||
pool.append(row)
|
||||
return pool
|
||||
|
||||
|
||||
## 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
|
||||
## boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
|
||||
## 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
|
||||
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
|
||||
if node_type != "boss" and node_type != "elite":
|
||||
return {}
|
||||
for row in variant_pool(level_index, board_type, node_type):
|
||||
if String(row.get("scope", "region")) == "global":
|
||||
return row
|
||||
return {}
|
||||
@@ -1,11 +1,12 @@
|
||||
[gd_resource type="Resource" script_class="AdventureTable" load_steps=2 format=3]
|
||||
[gd_resource type="Resource" script_class="AdventureTable" load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/adventure_table.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://data/schemas/adventure_quota_segment.gd" id="2"]
|
||||
[ext_resource type="Script" path="res://data/schemas/adventure_value_segment.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://data/schemas/adventure_cap_segment.gd" id="4"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
clear_heal_pct = 20
|
||||
curio_candidate_count = 3
|
||||
[sub_resource type="Resource" id="AdventureTable_quota"]
|
||||
script = ExtResource("2")
|
||||
curio_quota_attack_common = 18
|
||||
curio_quota_attack_excellent = 5
|
||||
curio_quota_max_hp_common = 17
|
||||
@@ -17,6 +18,9 @@ curio_quota_gp_excellent = 5
|
||||
curio_quota_heal_excellent = 3
|
||||
curio_quota_refresh_excellent = 2
|
||||
curio_quota_ap_rare = 3
|
||||
|
||||
[sub_resource type="Resource" id="AdventureTable_value"]
|
||||
script = ExtResource("3")
|
||||
curio_value_attack_common = 1
|
||||
curio_value_attack_excellent = 2
|
||||
curio_value_max_hp_common = 5
|
||||
@@ -28,26 +32,18 @@ curio_value_gp_excellent = 50
|
||||
curio_value_heal_excellent = 6
|
||||
curio_value_refresh_excellent = 1
|
||||
curio_value_ap_rare = 1
|
||||
|
||||
[sub_resource type="Resource" id="AdventureTable_cap"]
|
||||
script = ExtResource("4")
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
clear_heal_pct = 20
|
||||
curio_candidate_count = 3
|
||||
quota_segment = SubResource("AdventureTable_quota")
|
||||
value_segment = SubResource("AdventureTable_value")
|
||||
cap_segment = SubResource("AdventureTable_cap")
|
||||
feat_ticket_chance = 20
|
||||
feat_ticket_quota = 2
|
||||
feat_ticket_limit = 1
|
||||
feat_ticket_pity_step = 5
|
||||
|
||||
; 迭代 20 T2 池结构案D 换代(2026-10-11,奇物.md 池结构与抽取 v1):四级档位 70/25/7/0 = 102 枚——
|
||||
; 稀有档投放三 kind=AP 件 3/储备金袋 2/时沙 2(ap_rare 显式载于本表,与 schema 缺省同值);
|
||||
; 概率与上限解耦(案②令牌池+提前退役):概率位=档+令牌数、已拿上限=curio_cap_*(拿了才计);
|
||||
; 曝光槽=三选一 2 格全池+1 格优秀档以上子池(子池空回退全池);凭证不进子池。
|
||||
; 前史 TP0 令牌池换代(2026-10-03,node-reward v2.2 追认 b 落地):三选一池 = 固定配额令牌池——
|
||||
; 洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭;
|
||||
; 种类次数上限退役(池消耗即上限;货架库存同账本=牌库剩余令牌数)。
|
||||
; 档位构成(现行):普通〔小GP 18/小增伤 18/小护甲 17/小生命 17〕+ 优秀〔大GP/大增伤/大护甲/大生命 各 5+恢复 3+刷新件 2〕
|
||||
; + 稀有〔AP 件 3/储备金袋 2/时沙 2〕。
|
||||
; 数值对:小GP 20/大GP 50、小增伤 +1/大 +2、小护甲 +1/大 +2、小生命 +5/大 +10、恢复 +6(当即回血)、AP +1。
|
||||
; 恢复件 = 新入池种类(优秀档)。
|
||||
; 退役字段:bonus_weight_*(E-S 权重族)/ bonus_cap_*(种类次数上限族)/ BONUS_CAP_UNLIMITED 哨兵——
|
||||
; 洗牌改令牌 Fisher-Yates(不放回抽 3,消费 reward_pick 封闭流)。
|
||||
; 迭代 11 第②批 商店共享池注记:货架 = 令牌级抽签(同 kind 去重每 kind 至多一卡、价格公式 tier-blind
|
||||
; ceil(基价×b^(关序−1)×A^visit) 维持批②裁定、购买消费在架令牌本体)。
|
||||
; 末关的关卡完成不发三选一与回血(奖励只朝下一战生效)。
|
||||
; 全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss;档位配额结构锚 70/25/7/0——
|
||||
; schema TIER_QUOTAS,validate 硬断言,改总量须重裁)。
|
||||
@@ -73,7 +73,7 @@ static func boss_name(runner: Variant, db: ConfigDb) -> String:
|
||||
if runner != null:
|
||||
var node: Dictionary[Variant, Variant] = runner.current_node()
|
||||
if String(node.get("type", "")) == "boss":
|
||||
var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")
|
||||
var rowset: Array[Variant] = db.combat.enemy.resolve_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")
|
||||
if not rowset.is_empty():
|
||||
return String(rowset[0].get("display_name", ""))
|
||||
return "遗迹守卫"
|
||||
@@ -98,31 +98,36 @@ static func curio_pick_header(payload: Dictionary) -> String:
|
||||
# 对齐 v2.2 追认 b 口径:小/大前缀(小GP/大GP 族)、恢复/AP 不带档位词;道具件掷定前置时
|
||||
# treasure_line = 具体道具「名 · 效果摘要」行——常空走通用分支)
|
||||
static func bonus_card_text(id: String, tier: String, value: int, treasure_line: String = "") -> String:
|
||||
var ts := ""
|
||||
if tier == "common":
|
||||
ts = " · 小"
|
||||
elif tier == "rare":
|
||||
ts = " · 大"
|
||||
match id:
|
||||
"attack":
|
||||
return "攻击力提升%s\n攻击 +%d" % [ts, value]
|
||||
"max_hp":
|
||||
return "生命上限提升%s\n生命上限 +%d" % [ts, value]
|
||||
"armor":
|
||||
return "护甲等级提升%s\n护甲 +%d" % [ts, value]
|
||||
"gp":
|
||||
return "GP 袋%s\nGP +%d" % [ts, value]
|
||||
"heal":
|
||||
return "恢复药剂\n生命 +%d(当即回复)" % value
|
||||
"treasure_item":
|
||||
if treasure_line != "":
|
||||
return "秘宝\n%s" % treasure_line
|
||||
return "秘宝\n随机获得一件"
|
||||
"ap":
|
||||
return "行动点提升\n每战首轮 AP +%d" % value
|
||||
if id == "treasure_item":
|
||||
return _bonus_treasure_line(treasure_line)
|
||||
var ts := _tier_suffix(tier)
|
||||
var templates := {"attack": "攻击力提升%s\n攻击 +%d", "max_hp": "生命上限提升%s\n生命上限 +%d",
|
||||
"armor": "护甲等级提升%s\n护甲 +%d", "gp": "GP 袋%s\nGP +%d"}
|
||||
if templates.has(id):
|
||||
return templates[id] % [ts, value]
|
||||
if id == "heal":
|
||||
return "恢复药剂\n生命 +%d(当即回复)" % value
|
||||
if id == "ap":
|
||||
return "行动点提升\n每战首轮 AP +%d" % value
|
||||
return id
|
||||
|
||||
|
||||
## 档位尾注域(common=小/rare=大——旧载荷防御口径)
|
||||
static func _tier_suffix(tier: String) -> String:
|
||||
if tier == "common":
|
||||
return " · 小"
|
||||
if tier == "rare":
|
||||
return " · 大"
|
||||
return ""
|
||||
|
||||
|
||||
## 秘宝卡文案域(treasure_line 空串=随机获得一件)
|
||||
static func _bonus_treasure_line(treasure_line: String) -> String:
|
||||
if treasure_line != "":
|
||||
return "秘宝\n%s" % treasure_line
|
||||
return "秘宝\n随机获得一件"
|
||||
|
||||
|
||||
# 三选一兑付反馈行(选中件效果 + 丢弃提示;tier 缺省普通档——旧载荷防御口径)
|
||||
static func bonus_result_text(result: Dictionary) -> String:
|
||||
var name := ""
|
||||
|
||||
@@ -19,8 +19,8 @@ func _init(db_ref: ConfigDb, debug_overlay_ref: DebugOverlay, settlement_getter:
|
||||
# 禁表写路径(双写叠加隐患源头);
|
||||
# 合成式单点在 UiKit.scaled——与调试浮窗生效值预览共用,本类不持有同式私有实现)
|
||||
func debug_hero_override() -> Dictionary:
|
||||
return {"atk": UiKit.scaled(int(_db.hero.atk), _debug_overlay.atk_pct()),
|
||||
"max_hp": UiKit.scaled(int(_db.hero.max_hp), _debug_overlay.hp_pct())}
|
||||
return {"atk": UiKit.scaled(int(_db.combat.hero.atk), _debug_overlay.atk_pct()),
|
||||
"max_hp": UiKit.scaled(int(_db.combat.hero.max_hp), _debug_overlay.hp_pct())}
|
||||
|
||||
|
||||
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音;
|
||||
@@ -45,7 +45,7 @@ func variant_mask_lookup(c: Vector2i) -> bool:
|
||||
if b == null:
|
||||
return false
|
||||
for vid in b.variant_ids(c):
|
||||
var vrow: Dictionary[Variant, Variant] = _db.variation.row_by_id(String(vid))
|
||||
var vrow: Dictionary[Variant, Variant] = _db.combat.variation.row_by_id(String(vid))
|
||||
if String(vrow.get("effect", "")) == "hide_number":
|
||||
return true
|
||||
return false
|
||||
@@ -61,7 +61,7 @@ func variant_mark_lookup(c: Vector2i) -> String:
|
||||
if b == null:
|
||||
return ""
|
||||
for vid in b.variant_ids(c):
|
||||
var vrow: Dictionary[Variant, Variant] = _db.variation.row_by_id(String(vid))
|
||||
var vrow: Dictionary[Variant, Variant] = _db.combat.variation.row_by_id(String(vid))
|
||||
if String(vrow.get("effect", "")) == "flat_damage":
|
||||
return "flat_damage"
|
||||
return ""
|
||||
@@ -4,33 +4,16 @@ extends RefCounted
|
||||
# 原 13 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。
|
||||
# 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。
|
||||
|
||||
## 三束按域收拢(u21——同 u20 形;views=视图句柄/getters=查询 Callable 活引用/flow=编排 Callable):
|
||||
## 原 12 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。
|
||||
var db
|
||||
var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda)
|
||||
var save_get: Callable # () -> SaveManagerCore
|
||||
var camp_overlay: CampOverlay
|
||||
var shop_overlay: ShopOverlay
|
||||
var toast: ToastStack
|
||||
var start_overlay: Control
|
||||
var logger # PlaytestLogger(试玩日志验收取数面)
|
||||
var runner_get: Callable # () -> LevelRunner
|
||||
var open_modal: Callable
|
||||
var close_modal: Callable
|
||||
var refresh_presentations: Callable
|
||||
var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger}
|
||||
var getters: Dictionary = {} # {meta_get, save_get, runner_get}
|
||||
var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations}
|
||||
|
||||
|
||||
func _init(db_ref, meta_get_cb: Callable, save_get_cb: Callable, camp_overlay_ref: CampOverlay,
|
||||
shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control,
|
||||
logger_ref, runner_get_cb: Callable, open_modal_cb: Callable, close_modal_cb: Callable,
|
||||
refresh_presentations_cb: Callable) -> void:
|
||||
func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void:
|
||||
db = db_ref
|
||||
meta_get = meta_get_cb
|
||||
save_get = save_get_cb
|
||||
camp_overlay = camp_overlay_ref
|
||||
shop_overlay = shop_overlay_ref
|
||||
toast = toast_ref
|
||||
start_overlay = start_overlay_ref
|
||||
logger = logger_ref
|
||||
runner_get = runner_get_cb
|
||||
open_modal = open_modal_cb
|
||||
close_modal = close_modal_cb
|
||||
refresh_presentations = refresh_presentations_cb
|
||||
views = views_bundle
|
||||
getters = getters_bundle
|
||||
flow = flow_bundle
|
||||
@@ -21,17 +21,17 @@ var _refresh_presentations: Callable
|
||||
func _init(ctx: CampFlowContext) -> void:
|
||||
# 构造参数收拢 context 对象(13 参长签名收一处命名面)
|
||||
db = ctx.db
|
||||
_meta_get = ctx.meta_get
|
||||
_save_get = ctx.save_get
|
||||
camp_overlay = ctx.camp_overlay
|
||||
shop_overlay = ctx.shop_overlay
|
||||
toast = ctx.toast
|
||||
start_overlay = ctx.start_overlay
|
||||
logger = ctx.logger
|
||||
_runner_get = ctx.runner_get
|
||||
_open_modal = ctx.open_modal
|
||||
_close_modal = ctx.close_modal
|
||||
_refresh_presentations = ctx.refresh_presentations
|
||||
_meta_get = ctx.getters.meta_get
|
||||
_save_get = ctx.getters.save_get
|
||||
camp_overlay = ctx.views.camp_overlay
|
||||
shop_overlay = ctx.views.shop_overlay
|
||||
toast = ctx.views.toast
|
||||
start_overlay = ctx.views.start_overlay
|
||||
logger = ctx.views.logger
|
||||
_runner_get = ctx.getters.runner_get
|
||||
_open_modal = ctx.flow.open_modal
|
||||
_close_modal = ctx.flow.close_modal
|
||||
_refresh_presentations = ctx.flow.refresh_presentations
|
||||
|
||||
|
||||
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
|
||||
@@ -59,13 +59,13 @@ func on_camp_closed() -> void:
|
||||
func camp_data() -> Dictionary:
|
||||
var meta = _meta_get.call()
|
||||
return {
|
||||
"balance": meta.keystone_balance() if meta != null else 0,
|
||||
"balance": meta.meta_state.keystone_balance() if meta != null else 0,
|
||||
"workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影)
|
||||
"forge": meta.forge_shop_status() if meta != null else [], # 锻造工坊两区六路线投影
|
||||
"attune": meta.attune_camp_status() if meta != null else {}, # 同调装配面投影
|
||||
"tree": meta.tree_camp_rows() if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行)
|
||||
"fork_groups": meta.tree_fork_groups() if meta != null else [],
|
||||
"recent": _save_get.call().recent_summaries() if _save_get.call() != null else [],
|
||||
"tree": meta.tree_camp_status()["rows"] if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行)
|
||||
"fork_groups": meta.tree_camp_status()["fork_groups"] if meta != null else [],
|
||||
"recent": _save_get.call().meta_state.career_state()["recent_summaries"] if _save_get.call() != null else [],
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ func on_camp_tree_buy(node_id: String) -> void:
|
||||
var meta = _meta_get.call()
|
||||
if meta == null:
|
||||
return
|
||||
var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
|
||||
var ok: bool = meta.tree_buy_fork(node_id) if String(db.crossrun.meta_tree.row_by_id(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
|
||||
if not ok:
|
||||
toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
|
||||
return
|
||||
@@ -157,8 +157,8 @@ func on_camp_forge_select(route_id: String) -> void:
|
||||
var meta = _meta_get.call()
|
||||
if meta == null:
|
||||
return
|
||||
var ok: bool = meta.forge_select_weapon(route_id) if String(db.forge_route(route_id).get("kind", "")) == "weapon" \
|
||||
else meta.forge_select_armor(route_id)
|
||||
var ok: bool = meta.forge_select("weapon", route_id) if String(db.crossrun.forge.route(route_id).get("kind", "")) == "weapon" \
|
||||
else meta.forge_select("armor", route_id)
|
||||
if not ok:
|
||||
toast.show_toast("切换被拒——路线未解锁", "warn")
|
||||
return
|
||||
@@ -177,7 +177,7 @@ func on_shop_buy(product_id: String) -> void:
|
||||
toast.show_toast("GP不足——买不起这件商品", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
|
||||
_refresh_presentations.call()
|
||||
return
|
||||
@@ -199,7 +199,7 @@ func on_shop_refresh() -> void:
|
||||
toast.show_toast("GP不足——刷新失败", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -4,42 +4,16 @@ extends RefCounted
|
||||
# 原 16 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。
|
||||
# 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。
|
||||
|
||||
## 三束按域收拢(u20——views=视图句柄/getters=查询 Callable 活引用〔禁快照〕/flow=编排 Callable):
|
||||
## 原 16 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。
|
||||
var db
|
||||
var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda)
|
||||
var curio_overlay: CurioPickOverlay
|
||||
var end_overlay: BattleEndOverlay
|
||||
var board_view: BoardView
|
||||
var toast: ToastStack
|
||||
var save_get: Callable # () -> SaveManagerCore
|
||||
var adventure_get: Callable # () -> AdventureRunner(会话现值)
|
||||
var runner_get: Callable # () -> LevelRunner
|
||||
var adventure_mode_get: Callable # () -> bool
|
||||
var open_modal: Callable # (Control) -> void
|
||||
var open_modal_after_playback: Callable
|
||||
var close_modal: Callable
|
||||
var refresh_presentations: Callable
|
||||
var fatal_screen: Callable
|
||||
var gp_summary: Callable # (Dictionary) -> String
|
||||
var views: Dictionary = {} # {curio_overlay, end_overlay, board_view, toast}
|
||||
var getters: Dictionary = {} # {meta_get, save_get, adventure_get, runner_get, adventure_mode_get}
|
||||
var flow: Dictionary = {} # {open_modal, open_modal_after_playback, close_modal, refresh_presentations, fatal_screen, gp_summary}
|
||||
|
||||
|
||||
func _init(db_ref, meta_get_cb: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay,
|
||||
board_view_ref: BoardView, toast_ref: ToastStack, save_get_cb: Callable,
|
||||
adventure_get_cb: Callable, runner_get_cb: Callable, adventure_mode_get_cb: Callable,
|
||||
open_modal_cb: Callable, open_modal_after_playback_cb: Callable, close_modal_cb: Callable,
|
||||
refresh_presentations_cb: Callable, fatal_screen_cb: Callable, gp_summary_cb: Callable) -> void:
|
||||
func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void:
|
||||
db = db_ref
|
||||
meta_get = meta_get_cb
|
||||
curio_overlay = curio_overlay_ref
|
||||
end_overlay = end_overlay_ref
|
||||
board_view = board_view_ref
|
||||
toast = toast_ref
|
||||
save_get = save_get_cb
|
||||
adventure_get = adventure_get_cb
|
||||
runner_get = runner_get_cb
|
||||
adventure_mode_get = adventure_mode_get_cb
|
||||
open_modal = open_modal_cb
|
||||
open_modal_after_playback = open_modal_after_playback_cb
|
||||
close_modal = close_modal_cb
|
||||
refresh_presentations = refresh_presentations_cb
|
||||
fatal_screen = fatal_screen_cb
|
||||
gp_summary = gp_summary_cb
|
||||
views = views_bundle
|
||||
getters = getters_bundle
|
||||
flow = flow_bundle
|
||||
@@ -8,44 +8,16 @@ extends RefCounted
|
||||
const Narrator := preload("res://presentation/battle_narrator.gd")
|
||||
|
||||
var db
|
||||
var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面)
|
||||
var curio_overlay: CurioPickOverlay
|
||||
var end_overlay: BattleEndOverlay
|
||||
var board_view: BoardView
|
||||
var toast: ToastStack
|
||||
var _save_get: Callable # () → 存档承载现值(同上——Attunement.slots 读数面)
|
||||
var _adventure_get: Callable # () → 冒险结构现值(组合根 lambda——随局更替)
|
||||
var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——reward_node 继续推进)
|
||||
var _adventure_mode_get: Callable # () → 冒险模式标记现值(组合根 lambda)
|
||||
var _open_modal: Callable # 模态栈单口(组合根)
|
||||
var _open_modal_after_playback: Callable # 演播后排窗单口(组合根)
|
||||
var _close_modal: Callable
|
||||
var _refresh_presentations: Callable
|
||||
var _fatal_screen: Callable
|
||||
var _gp_summary: Callable # () → 战斗总结 GP 行文案(组合根 lambda——_last_gain_amount 取数面在根)
|
||||
var ctx: CurioFlowContext # u13 引用束注入:构造注入 context 活引用(views/getters/flow 三束键名)
|
||||
# _pending_curio/_curio_context 纯内部态保持宿主
|
||||
|
||||
var _pending_curio := {} # 三选一停点载荷暂存(战斗总结结算确认后开三选一面;续玩停点复用)
|
||||
var _curio_context := "level_clear" # 三选一面上下文(level_clear=关卡完成停点→推进下一关 / reward_node=奖励节点→回图选路)
|
||||
|
||||
|
||||
func _init(ctx: CurioFlowContext) -> void:
|
||||
# 构造参数收拢 context 对象(16 参长签名收一处命名面)
|
||||
func _init(ctx_ref: CurioFlowContext) -> void:
|
||||
ctx = ctx_ref
|
||||
db = ctx.db
|
||||
_meta_get = ctx.meta_get
|
||||
curio_overlay = ctx.curio_overlay
|
||||
end_overlay = ctx.end_overlay
|
||||
board_view = ctx.board_view
|
||||
toast = ctx.toast
|
||||
_save_get = ctx.save_get
|
||||
_adventure_get = ctx.adventure_get
|
||||
_runner_get = ctx.runner_get
|
||||
_adventure_mode_get = ctx.adventure_mode_get
|
||||
_open_modal = ctx.open_modal
|
||||
_open_modal_after_playback = ctx.open_modal_after_playback
|
||||
_close_modal = ctx.close_modal
|
||||
_refresh_presentations = ctx.refresh_presentations
|
||||
_fatal_screen = ctx.fatal_screen
|
||||
_gp_summary = ctx.gp_summary
|
||||
|
||||
|
||||
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
|
||||
@@ -66,17 +38,17 @@ func resume_if_pending() -> bool:
|
||||
# 差异仅零面可收——curio_overlay 未开不 close。core 停点经 skip_empty_curio 消化后推进
|
||||
#(continue_to_next_level 的「停点未兑付拒推进」守卫因此放行)。
|
||||
func _skip_empty_stop() -> bool:
|
||||
toast.show_toast("可用战利品为空——本次三选一跳过", "info")
|
||||
var adventure = _adventure_get.call()
|
||||
ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info")
|
||||
var adventure = ctx.getters.adventure_get.call()
|
||||
if adventure != null:
|
||||
adventure.stop().skip_empty_curio() # core 停点消化(其守卫=仅空候选放行)
|
||||
if _curio_context == "reward_node":
|
||||
var runner = _runner_get.call()
|
||||
var runner = ctx.getters.runner_get.call()
|
||||
if runner != null:
|
||||
runner.continue_after_node()
|
||||
elif _curio_context == "level_clear" and bool(_adventure_mode_get.call()):
|
||||
elif _curio_context == "level_clear" and bool(ctx.getters.adventure_mode_get.call()):
|
||||
if not adventure.continue_to_next_level():
|
||||
_fatal_screen.call("关卡推进失败——见输出日志")
|
||||
ctx.flow.fatal_screen.call("关卡推进失败——见输出日志")
|
||||
# feat_ticket = 只消化停点回店(零推进——与 on_continue 同构)
|
||||
_pending_curio = {}
|
||||
return true
|
||||
@@ -93,41 +65,41 @@ func on_presented(payload: Dictionary) -> void:
|
||||
# 空候选防御(零卡不开死面;core _redeem_feat_ticket 守卫外的末道)
|
||||
_skip_empty_stop()
|
||||
return
|
||||
_open_modal.call(curio_overlay)
|
||||
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array))
|
||||
ctx.flow.open_modal.call(ctx.views.curio_overlay)
|
||||
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array))
|
||||
return
|
||||
if int(payload.get("aux_cleared", 0)) > 0:
|
||||
toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈
|
||||
ctx.views.toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈
|
||||
_pending_curio = payload.duplicate(true)
|
||||
_curio_context = "level_clear"
|
||||
if bool(payload.get("refreshed", false)):
|
||||
# 刷新重发:refreshed 标直落选择面——不再重开胜利总结面
|
||||
show_pick()
|
||||
return
|
||||
end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
|
||||
board_view.get_global_rect(),
|
||||
"%s\n%s" % [String(_gp_summary.call()), Narrator.bonus_keystone_line(payload)])
|
||||
_open_modal_after_playback.call(end_overlay)
|
||||
_refresh_presentations.call()
|
||||
ctx.views.end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
|
||||
ctx.views.board_view.get_global_rect(),
|
||||
"%s\n%s" % [String(ctx.flow.gp_summary.call()), Narrator.bonus_keystone_line(payload)])
|
||||
ctx.flow.open_modal_after_playback.call(ctx.views.end_overlay)
|
||||
ctx.flow.refresh_presentations.call()
|
||||
|
||||
|
||||
# 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
|
||||
# 无替换子交互(同调装配不可变,见类头)。
|
||||
func present_node_curio() -> void:
|
||||
var adventure = _adventure_get.call()
|
||||
var adventure = ctx.getters.adventure_get.call()
|
||||
_curio_context = "reward_node"
|
||||
var node_draw: Dictionary[Variant, Variant] = adventure.stop().node_curio_candidates()
|
||||
var node_cands: Array[Variant] = node_draw.get("candidates", []) as Array
|
||||
if node_cands.is_empty():
|
||||
# 空池降级——不开面、不挂节点停点(present_node_curio 未调即未挂),
|
||||
# 兑付确认直落回图选路(auto-sim「node_entered 回调内立即 continue_after_node」先例)
|
||||
toast.show_toast("可用战利品为空——本次三选一跳过", "info")
|
||||
var runner = _runner_get.call()
|
||||
ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info")
|
||||
var runner = ctx.getters.runner_get.call()
|
||||
if runner != null:
|
||||
runner.continue_after_node()
|
||||
return
|
||||
_open_modal.call(curio_overlay)
|
||||
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands))
|
||||
ctx.flow.open_modal.call(ctx.views.curio_overlay)
|
||||
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands))
|
||||
adventure.stop().present_node_curio(node_cands)
|
||||
_feed_refresh_display(adventure) # 刷新面投喂单点
|
||||
|
||||
@@ -135,20 +107,20 @@ func present_node_curio() -> void:
|
||||
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复)
|
||||
func show_pick() -> void:
|
||||
var payload: Dictionary[Variant, Variant] = _pending_curio
|
||||
_open_modal.call(curio_overlay)
|
||||
curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array))
|
||||
var adventure = _adventure_get.call()
|
||||
ctx.flow.open_modal.call(ctx.views.curio_overlay)
|
||||
ctx.views.curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array))
|
||||
var adventure = ctx.getters.adventure_get.call()
|
||||
if adventure != null:
|
||||
_feed_refresh_display(adventure) # 刷新面投喂单点
|
||||
_refresh_presentations.call()
|
||||
ctx.flow.refresh_presentations.call()
|
||||
|
||||
|
||||
# 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构)
|
||||
func _feed_refresh_display(adventure: AdventureRunner) -> void:
|
||||
var runner = _runner_get.call()
|
||||
var runner = ctx.getters.runner_get.call()
|
||||
var gp := int(runner.gp) if runner != null else 0
|
||||
var cost: int = adventure.refresh_bank().gp_cost()
|
||||
curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost)
|
||||
ctx.views.curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost)
|
||||
|
||||
|
||||
func _curio_card_texts(candidates: Array) -> Array:
|
||||
@@ -165,9 +137,9 @@ func _curio_card_texts(candidates: Array) -> Array:
|
||||
text = "专长获取
|
||||
选中或买下后立刻开一次专长三选一"
|
||||
elif kind == "relic":
|
||||
text = Narrator.feat_card_text(db.feat_row(rid))
|
||||
text = Narrator.feat_card_text(db.crossrun.feats.row_by_id(rid))
|
||||
else:
|
||||
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)))
|
||||
text = Narrator.bonus_card_text(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
|
||||
out.append({"id": cid, "text": text})
|
||||
return out
|
||||
|
||||
@@ -180,29 +152,29 @@ func _curio_card_texts(candidates: Array) -> Array:
|
||||
# 同 shop_refresh_cost_base/curve 旋钮、同冒险钱包 runner.gp;关卡完成停点经重发 curio_choice_presented 直落
|
||||
# 选择面,节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
|
||||
func on_refresh() -> void:
|
||||
var adventure = _adventure_get.call()
|
||||
var adventure = ctx.getters.adventure_get.call()
|
||||
if adventure == null or not adventure.stop().curio_stop_pending():
|
||||
return
|
||||
var runner = _runner_get.call()
|
||||
var runner = ctx.getters.runner_get.call()
|
||||
var refreshed: bool = adventure.stop().refresh_curio()
|
||||
if not refreshed:
|
||||
# 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用)
|
||||
var cost: int = adventure.refresh_bank().gp_cost()
|
||||
if runner == null or not runner.spend_gp(cost):
|
||||
toast.show_toast("GP不足——刷新失败", "warn")
|
||||
ctx.views.toast.show_toast("GP不足——刷新失败", "warn")
|
||||
return
|
||||
refreshed = adventure.stop().refresh_curio_paid()
|
||||
_refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面)
|
||||
ctx.flow.refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面)
|
||||
if not refreshed:
|
||||
return
|
||||
if _curio_context == "reward_node":
|
||||
var st: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state()
|
||||
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array))
|
||||
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array))
|
||||
_feed_refresh_display(adventure)
|
||||
|
||||
|
||||
func on_pick_confirmed(item_id: String) -> void:
|
||||
var adventure = _adventure_get.call()
|
||||
var adventure = ctx.getters.adventure_get.call()
|
||||
if adventure == null:
|
||||
return
|
||||
# TP0 令牌池——卡片 id "kind|tier|feat_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段)
|
||||
@@ -218,39 +190,39 @@ func on_pick_confirmed(item_id: String) -> void:
|
||||
# 后续兑付/确认仍走节点路径回图选路)
|
||||
if _curio_context == "reward_node" and kind == "feat_ticket":
|
||||
var st2: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state()
|
||||
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array))
|
||||
ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array))
|
||||
|
||||
|
||||
# 兑付反馈(curio_resolved 到达——结果行上屏,唯一出口 = 继续)
|
||||
func on_resolved(result: Dictionary) -> void:
|
||||
_pending_curio = {}
|
||||
curio_overlay.show_result(Narrator.bonus_result_text(result))
|
||||
_refresh_presentations.call()
|
||||
ctx.views.curio_overlay.show_result(Narrator.bonus_result_text(result))
|
||||
ctx.flow.refresh_presentations.call()
|
||||
|
||||
|
||||
# 继续钮:收三选一面 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
|
||||
func on_continue() -> void:
|
||||
_close_modal.call(curio_overlay)
|
||||
curio_overlay.close_pick()
|
||||
var adventure = _adventure_get.call()
|
||||
ctx.flow.close_modal.call(ctx.views.curio_overlay)
|
||||
ctx.views.curio_overlay.close_pick()
|
||||
var adventure = ctx.getters.adventure_get.call()
|
||||
if _curio_context == "feat_ticket":
|
||||
return # T5 店内专长凭证面——专长兑付完成后关面回店(无推进/无回图)
|
||||
if _curio_context == "reward_node":
|
||||
var runner = _runner_get.call()
|
||||
var runner = ctx.getters.runner_get.call()
|
||||
if runner != null:
|
||||
runner.continue_after_node()
|
||||
return
|
||||
if bool(_adventure_mode_get.call()) and adventure != null:
|
||||
if bool(ctx.getters.adventure_mode_get.call()) and adventure != null:
|
||||
if not adventure.continue_to_next_level():
|
||||
_fatal_screen.call("关卡推进失败——见输出日志")
|
||||
ctx.flow.fatal_screen.call("关卡推进失败——见输出日志")
|
||||
|
||||
|
||||
# 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator);
|
||||
# 数据源=treasure 装配词表 12 件
|
||||
func item_name_lookup() -> Dictionary:
|
||||
var lookup := {}
|
||||
var meta = _meta_get.call()
|
||||
var meta = ctx.getters.meta_get.call()
|
||||
if meta != null:
|
||||
for row in db.treasure_attunement_rows():
|
||||
for row in db.crossrun.treasure_items.attunement_rows():
|
||||
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
|
||||
return lookup
|
||||
@@ -11,8 +11,13 @@ const Narrator := preload("res://presentation/battle_narrator.gd")
|
||||
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
|
||||
# 钱包 GP/钥石不在本装配面投喂(中/大循环资源迁出战斗屏——信息归属单一展示面原则);
|
||||
# HP/状态效果单投喂 B 区站桩(B/C 双投喂属重复展示——单一展示面原则)
|
||||
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
|
||||
settlement, runner, level_active: bool, db, refresh_remaining: int) -> void:
|
||||
# 运行态尾参收拢 ctx 束(u19——键 {settlement, runner, level_active, db, refresh_remaining})
|
||||
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, ctx: Dictionary) -> void:
|
||||
var settlement = ctx.get("settlement")
|
||||
var runner = ctx.get("runner")
|
||||
var level_active: bool = ctx.get("level_active", false)
|
||||
var db = ctx.get("db")
|
||||
var refresh_remaining: int = ctx.get("refresh_remaining", 0)
|
||||
# 特性列表(runner 统一加算区投影:商店购买与关卡大奖励
|
||||
# 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。
|
||||
# 呈现只读投影零核心触碰;行文格式=白名单项)
|
||||
@@ -36,7 +41,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB
|
||||
info.set_feat_holdings([])
|
||||
if settlement == null:
|
||||
info.set_board_variants([]) # 战斗域角标出战斗即隐(空态纪律)
|
||||
_refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db)
|
||||
_refresh_out_of_battle(stage, info, ap_bar, ctx)
|
||||
return
|
||||
var s: Dictionary[Variant, Variant] = settlement.presentation_snapshot()
|
||||
# 地形变异常驻角标投影(整图变异快照 → 文案行;
|
||||
@@ -44,7 +49,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB
|
||||
info.set_board_variants(board_variant_lines(db, s.get("board_variants", [])))
|
||||
var hero: Dictionary[Variant, Variant] = s.hero
|
||||
if hero.is_empty() or not bool(hero.get("alive", false)):
|
||||
_refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db)
|
||||
_refresh_out_of_battle(stage, info, ap_bar, ctx)
|
||||
return
|
||||
_refresh_hero(stage, info, s)
|
||||
_refresh_economy(ap_bar, s, db, refresh_remaining)
|
||||
@@ -79,7 +84,7 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di
|
||||
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态——RAP 读数在本条) ----
|
||||
|
||||
static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, refresh_remaining: int) -> void:
|
||||
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
|
||||
ap_bar.set_ap(int(s.ap), int(db.run.action_economy.ap_per_round))
|
||||
ap_bar.set_rap(int(s.rap))
|
||||
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
|
||||
ap_bar.set_refresh_state(refresh_remaining,
|
||||
@@ -92,11 +97,11 @@ static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, ref
|
||||
# 无 archetype 字段,聚合形态会空键兜底实显「流派 ×N · 档一」僵尸文案;流派档位非现行机制)
|
||||
static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]:
|
||||
var rlines: Array[String] = []
|
||||
if db == null or db.feats == null or (db.feats.rows as Array).is_empty() or held.is_empty():
|
||||
if db == null or db.crossrun.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty():
|
||||
return rlines
|
||||
var counts := {}
|
||||
for rid in held:
|
||||
var rrow: Dictionary[Variant, Variant] = db.feat_row(String(rid))
|
||||
var rrow: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(String(rid))
|
||||
var fname := String(rrow.get("display_name", String(rid)))
|
||||
counts[fname] = int(counts.get(fname, 0)) + 1
|
||||
for fname in counts.keys():
|
||||
@@ -109,10 +114,10 @@ static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]:
|
||||
# 文案用常规表述,不用俚语
|
||||
static func board_variant_lines(db: ConfigDb, ids: Array) -> Array[String]:
|
||||
var lines: Array[String] = []
|
||||
if db == null or db.variation == null:
|
||||
if db == null or db.combat.variation == null:
|
||||
return lines
|
||||
for vid in ids:
|
||||
var row: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid))
|
||||
var row: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid))
|
||||
if not row.is_empty():
|
||||
lines.append(String(row.get("display_name", vid)))
|
||||
lines.append("回复与护甲资源减少")
|
||||
@@ -145,7 +150,10 @@ static func sync_settlement_visuals(stage: BattleStageView, board_view: BoardVie
|
||||
|
||||
# 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值
|
||||
static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView,
|
||||
ap_bar: ApBarView, runner, level_active: bool, db) -> void:
|
||||
ap_bar: ApBarView, ctx: Dictionary) -> void:
|
||||
var runner = ctx.get("runner")
|
||||
var level_active: bool = ctx.get("level_active", false)
|
||||
var db = ctx.get("db")
|
||||
if not level_active:
|
||||
return
|
||||
if runner != null:
|
||||
@@ -185,7 +193,14 @@ static func outcome_text(outcome: Dictionary) -> String:
|
||||
|
||||
# ---- 批量模拟报告装配(文案归 presenter——视图只 set_text) ----
|
||||
|
||||
static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String:
|
||||
## 统计束收拢(u19——stats 六键 {wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes})
|
||||
static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, stats: Dictionary, base_hp: int) -> String:
|
||||
var wins: int = stats.get("wins", 0)
|
||||
var dmg_all: Array = stats.get("dmg_all", [])
|
||||
var hp_left_win: Array = stats.get("hp_left_win", [])
|
||||
var hp_boss: Array = stats.get("hp_boss", [])
|
||||
var rounds_all: Array = stats.get("rounds_all", [])
|
||||
var death_nodes: Dictionary = stats.get("death_nodes", {})
|
||||
var lines: Array[String] = []
|
||||
lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct])
|
||||
lines.append("关卡完成率 [color=#%s]%.0f%%[/color](%d/%d)" % [UiKit.HEX_LOG_WIN, 100.0 * float(wins) / float(maxi(1, n)), wins, n])
|
||||
@@ -229,7 +244,7 @@ static func _p90(a: Array) -> float:
|
||||
# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链)
|
||||
static func status_data(db: ConfigDb, meta: MetaProgressionCore, runner: Variant, treasure_ui: Variant) -> Dictionary:
|
||||
return {
|
||||
"balance": meta.keystone_balance(),
|
||||
"balance": meta.meta_state.keystone_balance(),
|
||||
"hero_hp": int(runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命)
|
||||
"max_hp": runner.effective_max_hp(),
|
||||
"atk": runner.effective_atk(),
|
||||
@@ -257,20 +272,20 @@ static func status_curios(treasure_ui: Variant) -> Array:
|
||||
static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaProgressionCore) -> Dictionary:
|
||||
var clears: Array[Dictionary] = []
|
||||
var boss_first := 0
|
||||
for i in db.level_count():
|
||||
for i in db.run.level.level_count():
|
||||
var li: int = i + 1
|
||||
clears.append({"display_name": String(db.level_row(li).get("display_name", "")),
|
||||
"count": save_manager.clear_count(li)})
|
||||
if save_manager.boss_first_kill_marked(li):
|
||||
clears.append({"display_name": String(db.run.level.row_by_level_index(li).get("display_name", "")),
|
||||
"count": save_manager.meta_state.career_state().clear_counts.get(str(li), 0)})
|
||||
if save_manager.meta_state.career_state().boss_first_kill.has(str(li)):
|
||||
boss_first += 1
|
||||
return {
|
||||
"total": save_manager.adventure_total(),
|
||||
"cleared": save_manager.adventure_cleared(),
|
||||
"total": save_manager.meta_state.career_state().adventure_total,
|
||||
"cleared": save_manager.meta_state.career_state().adventure_cleared,
|
||||
"clears": clears,
|
||||
"boss_first_kills": boss_first,
|
||||
"boss_first_total": db.level_count(),
|
||||
"boss_kills": save_manager.boss_kill_count(),
|
||||
"keystone": meta.keystone_balance(),
|
||||
"boss_first_total": db.run.level.level_count(),
|
||||
"boss_kills": save_manager.meta_state.career_state().boss_kill_count,
|
||||
"keystone": meta.meta_state.keystone_balance(),
|
||||
}
|
||||
|
||||
|
||||
@@ -280,7 +295,7 @@ static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaP
|
||||
static func save_select_data(save_manager: SaveManagerCore) -> Dictionary:
|
||||
var slots: Array[Dictionary] = []
|
||||
for i in range(1, save_manager.SLOT_COUNT + 1):
|
||||
var p := save_manager.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒)
|
||||
var p := save_manager.slots.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒)
|
||||
slots.append({"slot": i,
|
||||
"occupied": bool(p.get("has_file", false)) and bool(p.get("readable", false)),
|
||||
"keystone": int(p.get("keystone", 0)),
|
||||
|
||||
+21
-17
@@ -312,7 +312,7 @@ func _on_confirm_accepted() -> void:
|
||||
_close_modal(confirm_dialog) # 模态栈单口
|
||||
match _confirm_consumer:
|
||||
"delete_slot":
|
||||
save_manager.delete_slot(_confirm_consumer_slot)
|
||||
save_manager.slots.delete_slot(_confirm_consumer_slot)
|
||||
save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
|
||||
_confirm_consumer = ""
|
||||
|
||||
@@ -360,15 +360,15 @@ func _wire_adventure() -> void:
|
||||
# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects);
|
||||
# hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。
|
||||
# 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照;
|
||||
# 取值=meta.hero_attack_type/hero_armor_type=存档选中路线类型轴,未解锁/异常选中态回退锻造配置默认值)。
|
||||
# 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。
|
||||
# 注:runner 侧 resume_adventure 受理面收 attunement/attune_refresh_y/tree_rules 三键
|
||||
# (传参/受理两侧对齐)。
|
||||
static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary:
|
||||
var params: Dictionary[Variant, Variant] = {
|
||||
"hero_override": meta_ref.hero_override() if meta_ref != null else {},
|
||||
"attack_type": meta_ref.hero_attack_type() if meta_ref != null else "",
|
||||
"armor_type": meta_ref.hero_armor_type() if meta_ref != null else "",
|
||||
"attunement": save_manager_ref.attunement_equipped() if save_manager_ref != null else [],
|
||||
"attack_type": meta_ref.hero_type_axes()["attack"] if meta_ref != null else "",
|
||||
"armor_type": meta_ref.hero_type_axes()["armor"] if meta_ref != null else "",
|
||||
"attunement": save_manager_ref.meta_state.attune_state().equipped if save_manager_ref != null else [],
|
||||
"attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0,
|
||||
"tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [],
|
||||
}
|
||||
@@ -447,21 +447,17 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
match String(decision.outcome):
|
||||
"next_layer":
|
||||
_inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
|
||||
int(decision.layer), int(decision.carry_hp), int(db.hero.max_hp)]
|
||||
int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)]
|
||||
_inter_overlay.visible = true
|
||||
_refresh_presentations()
|
||||
return
|
||||
"level_end":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", true),
|
||||
"返回", board_view.get_global_rect(), _gp_summary_text(payload),
|
||||
"钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner),
|
||||
int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp))
|
||||
"返回", board_view.get_global_rect(), _end_victory_opts(payload))
|
||||
"standalone_end":
|
||||
if result == "victory":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
|
||||
"回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload),
|
||||
"钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner),
|
||||
int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp))
|
||||
"回到遭遇选择", board_view.get_global_rect(), _end_victory_opts(payload))
|
||||
else:
|
||||
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
|
||||
"返回", board_view.get_global_rect())
|
||||
@@ -482,6 +478,14 @@ func _gp_summary_text(payload: Dictionary) -> String:
|
||||
|
||||
# ==================== core↔view 接线(战斗信号统一走这里)====================
|
||||
|
||||
|
||||
## 战末账行束(open_victory opts 键装配单点——GP/钥石/加成三行与增量合计)
|
||||
func _end_victory_opts(payload: Dictionary) -> Dictionary:
|
||||
return {"gp_summary": _gp_summary_text(payload),
|
||||
"ks_summary": "钥石 · 累计 %d" % run_scope.runner.keystone,
|
||||
"bonus_summary": HudPresenter.bonus_summary_text(run_scope.runner),
|
||||
"gp_gain": int(payload.get("gp_dropped", 0)), "gp_total": int(run_scope.runner.gp)}
|
||||
|
||||
func _wire_battle(settlement_ref: BattleOrchestrator) -> void:
|
||||
settlement_ref.step_row.connect(_on_step_row)
|
||||
settlement_ref.battle_finished.connect(_on_battle_finished)
|
||||
@@ -772,11 +776,11 @@ func _on_refresh_confirmed() -> void:
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok)
|
||||
if ok:
|
||||
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()})
|
||||
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold")
|
||||
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
|
||||
else:
|
||||
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
|
||||
var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0
|
||||
var cost: int = int(db.action_economy.rap_refresh_cost)
|
||||
var cost: int = int(db.run.action_economy.rap_refresh_cost)
|
||||
var result := "rejected_rap" if rap < cost else "rejected_cap"
|
||||
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1})
|
||||
if result == "rejected_rap":
|
||||
@@ -811,8 +815,8 @@ func _refresh_used_snapshot() -> int:
|
||||
|
||||
func _refresh_presentations() -> void:
|
||||
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
||||
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining)
|
||||
var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
|
||||
_treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗)
|
||||
_sync_settlement_visuals()
|
||||
|
||||
@@ -906,6 +910,6 @@ func _seed_value() -> int:
|
||||
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费)
|
||||
if _seed_edit.text.is_valid_int():
|
||||
return int(_seed_edit.text)
|
||||
if save_manager.is_first_adventure_done():
|
||||
if save_manager.meta_state.career_state().first_adventure_done:
|
||||
return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免)
|
||||
return 42
|
||||
@@ -80,7 +80,7 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -
|
||||
root._open_modal(root.shop_overlay)
|
||||
if root.run_scope.runner.is_shop_pool():
|
||||
root.shop_overlay.open_shelf(root.run_scope.runner.shop_shelf(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.shop_price_a())) # 爬价规则行
|
||||
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
|
||||
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop_refresh_free_left(), root.run_scope.runner.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop_refresh_cost()) # MC-1
|
||||
else:
|
||||
root.shop_overlay.open_shop(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp))
|
||||
@@ -111,7 +111,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
|
||||
text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup())
|
||||
if result == "completed":
|
||||
root._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(),
|
||||
"本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账
|
||||
{"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账
|
||||
else:
|
||||
root._end_overlay.open_defeat(text, "返回", root.board_view.get_global_rect())
|
||||
root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
|
||||
@@ -178,7 +178,7 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void:
|
||||
root.run_scope.pending_candidates = candidates.duplicate()
|
||||
root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
|
||||
root.board_view.get_global_rect(),
|
||||
"战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)])
|
||||
{"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]})
|
||||
root._open_modal_after_playback(root._end_overlay)
|
||||
root._refresh_presentations()
|
||||
return
|
||||
|
||||
@@ -68,14 +68,14 @@ static func _on_save_select_closed(root: Variant) -> void:
|
||||
# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘,
|
||||
# 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。
|
||||
static func _on_save_select_new(root: Variant, slot: int) -> void:
|
||||
root.save_manager.create_slot(slot)
|
||||
root.save_manager.slots.create_slot(slot)
|
||||
root._camp_pres.open_camp()
|
||||
|
||||
|
||||
|
||||
# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。
|
||||
static func _on_save_select_load(root: Variant, slot: int) -> void:
|
||||
root.save_manager.activate_slot(slot)
|
||||
root.save_manager.slots.activate_slot(slot)
|
||||
root._camp_pres.open_camp()
|
||||
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ static func _build_battle_wiring(root: Variant) -> void:
|
||||
root.board_zone.chord_attempted.connect(root._on_chord_attempted)
|
||||
|
||||
root.logger = PlaytestLogger.new()
|
||||
root.logger.ap_per_round = int(root.db.action_economy.ap_per_round)
|
||||
root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round)
|
||||
|
||||
static func _build_overlays(root: Variant) -> void:
|
||||
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
|
||||
@@ -134,11 +134,17 @@ static func _build_curio(root: Variant) -> void:
|
||||
root.add_child(root.curio_overlay)
|
||||
# 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入,
|
||||
# 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调)
|
||||
root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, func() -> Variant: return root.meta, root.curio_overlay, root._end_overlay, root.board_view,
|
||||
root.board_zone.toast, func() -> Variant: return root.save_manager, func() -> Variant: return root.run_scope.adventure,
|
||||
func() -> Variant: return root.run_scope.runner, func() -> Variant: return root.run_scope.is_adventure,
|
||||
root._open_modal, root._open_modal_after_playback, root._close_modal, root._refresh_presentations,
|
||||
root._fatal_screen, func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}))) # 构造参数收拢 context
|
||||
root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db,
|
||||
{"curio_overlay": root.curio_overlay, "end_overlay": root._end_overlay,
|
||||
"board_view": root.board_view, "toast": root.board_zone.toast},
|
||||
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
|
||||
"adventure_get": func() -> Variant: return root.run_scope.adventure,
|
||||
"runner_get": func() -> Variant: return root.run_scope.runner,
|
||||
"adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure},
|
||||
{"open_modal": root._open_modal, "open_modal_after_playback": root._open_modal_after_playback,
|
||||
"close_modal": root._close_modal, "refresh_presentations": root._refresh_presentations,
|
||||
"fatal_screen": root._fatal_screen,
|
||||
"gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束)
|
||||
root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
|
||||
root.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
|
||||
root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
|
||||
@@ -154,9 +160,13 @@ static func _build_menu_wiring(root: Variant) -> void:
|
||||
root.debug_badge.pressed.connect(root._on_debug_badge_pressed)
|
||||
# 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发;
|
||||
# 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入)
|
||||
root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, func() -> Variant: return root.meta, func() -> Variant: return root.save_manager, root.camp_overlay, root.shop_overlay,
|
||||
root.board_zone.toast, root._start_overlay, root.logger, func() -> Variant: return root.run_scope.runner,
|
||||
root._open_modal, root._close_modal, root._refresh_presentations)) # 构造参数收拢 context
|
||||
root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db,
|
||||
{"camp_overlay": root.camp_overlay, "shop_overlay": root.shop_overlay,
|
||||
"toast": root.board_zone.toast, "start_overlay": root._start_overlay, "logger": root.logger},
|
||||
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
|
||||
"runner_get": func() -> Variant: return root.run_scope.runner},
|
||||
{"open_modal": root._open_modal, "close_modal": root._close_modal,
|
||||
"refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束)
|
||||
root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
|
||||
root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
|
||||
root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
|
||||
@@ -193,7 +203,7 @@ static func _build_debug_and_resolvers(root: Variant) -> void:
|
||||
# 诊断转储入口在本浮窗,玩家画面零常驻调试件)。
|
||||
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
|
||||
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
|
||||
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.hero.atk), int(root.db.hero.max_hp))
|
||||
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
|
||||
root.add_child(root.debug_overlay)
|
||||
root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变)
|
||||
root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay))
|
||||
@@ -235,9 +245,14 @@ static func _build_modal_flow(root: Variant) -> void:
|
||||
root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言)
|
||||
# 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表)
|
||||
root._apply_layer_contract()
|
||||
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.hero.max_hp))
|
||||
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp))
|
||||
|
||||
static func fatal_screen(root: Variant, msg: String) -> void:
|
||||
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
|
||||
l.set_anchors_preset(Control.PRESET_CENTER)
|
||||
root.add_child(l)
|
||||
|
||||
|
||||
# GP 总结行回调(gp_summary 束键——单行 lambda 内嵌字典实参收尾歧义,提函数承载)
|
||||
static func _gp_summary_cb(root: Variant) -> Callable:
|
||||
return func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount})
|
||||
@@ -157,8 +157,6 @@ func dump_diagnosis(context: Dictionary) -> String:
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(dump, " "))
|
||||
f.close()
|
||||
print("[playtest_logger] 诊断转储已写入: %s(项目日志目录见输出)" % path)
|
||||
print(" [diag] %s" % JSON.stringify(context))
|
||||
return path
|
||||
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ func unlocked_set() -> Dictionary:
|
||||
var out := {}
|
||||
if meta == null:
|
||||
return out
|
||||
for row in db.treasure_transitional_rows():
|
||||
for row in db.crossrun.treasure_items.transitional_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if meta.is_item_unlocked(iid):
|
||||
if meta.meta_state.item_unlocked(iid):
|
||||
out[iid] = true
|
||||
return out
|
||||
|
||||
@@ -79,7 +79,7 @@ func on_slot_clicked(slot_handle: int) -> void:
|
||||
break
|
||||
if rec.is_empty() or bool(rec.get("used", false)):
|
||||
return
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", "")))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(rec.get("item_id", "")))
|
||||
if String(arow.get("target", "none")) == "none":
|
||||
attempt_use(slot_handle) # AP 类:显式无目标即用(不再伪造哨兵坐标)
|
||||
return
|
||||
@@ -106,7 +106,7 @@ func attempt_use(handle: int, cell: Variant = null) -> void:
|
||||
refresh_slots()
|
||||
refresh_overlays()
|
||||
board_view.refresh() # 移除格即时落图(无移除态的件 diff 零变化幂等)
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.get("item_id", "")))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(v.get("item_id", "")))
|
||||
toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info")
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ func inventory_projection() -> Array:
|
||||
# 库存投影行(字形=名首字;usable=整图零信息预判回调接排雷玩法只读面)+
|
||||
# 积点进度浮窗行(自动兑付件 C 区悬停 tooltip 补 N/M 进度;件 id 锁定=装配行唯一)
|
||||
func _projection_row(rec: Dictionary, cs: Dictionary) -> Dictionary:
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", "")))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(rec.get("item_id", "")))
|
||||
var dn := String(arow.get("display_name", ""))
|
||||
var note := ""
|
||||
if bool(cs.get("assembled", false)) and String(cs.get("item_id", "")) == String(rec.get("item_id", "")):
|
||||
|
||||
@@ -38,33 +38,31 @@ func run(n: int) -> void:
|
||||
# 共享日志会话(模拟运行与真人试玩同日志合规——操作/轮次/战斗/关卡完成全量 JSONL;
|
||||
# 一文件多局,run 行携带种子切分局次)
|
||||
var logger = PlaytestLogger.new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
var log_path: String = logger.start_session(SEED_BASE)
|
||||
print("[tune_sim] 模拟日志: %s" % log_path)
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
|
||||
logger.start_session(SEED_BASE)
|
||||
panel.set_sim_running(true)
|
||||
panel.set_sim_progress("模拟中:0/%d …" % n)
|
||||
var wins := 0
|
||||
var dmg_all: Array[float] = []
|
||||
var hp_left_win: Array[float] = []
|
||||
var hp_boss: Array[float] = []
|
||||
var rounds_all: Array[int] = []
|
||||
var death_nodes := {}
|
||||
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
|
||||
var autoplay = _autoplay()
|
||||
for i in range(n):
|
||||
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
|
||||
dmg_all.append(st.hero_dmg)
|
||||
rounds_all.append(st.rounds)
|
||||
if st.result == "victory":
|
||||
wins += 1
|
||||
hp_left_win.append(st.hp_left)
|
||||
hp_boss.append(st.hp_at_boss)
|
||||
elif st.result == "defeat":
|
||||
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
|
||||
death_nodes[key] = int(death_nodes.get(key, 0)) + 1
|
||||
_run_one_level(autoplay, stats, i, panel, logger)
|
||||
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
|
||||
await panel.get_tree().process_frame
|
||||
logger.close_session()
|
||||
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(),
|
||||
wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes, _base_hp))
|
||||
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
|
||||
|
||||
|
||||
## 单局模拟域(循环体提函数——结果四桶记账;u19 stats 六键同构承载)
|
||||
func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void:
|
||||
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
|
||||
(stats["dmg_all"] as Array).append(st.hero_dmg)
|
||||
(stats["rounds_all"] as Array).append(st.rounds)
|
||||
if st.result == "victory":
|
||||
stats["wins"] = int(stats["wins"]) + 1
|
||||
(stats["hp_left_win"] as Array).append(st.hp_left)
|
||||
(stats["hp_boss"] as Array).append(st.hp_at_boss)
|
||||
elif st.result == "defeat":
|
||||
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
|
||||
stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1
|
||||
panel.set_sim_running(false)
|
||||
_running = false
|
||||
@@ -32,7 +32,7 @@ static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary:
|
||||
|
||||
var board_zone := BoardZoneView.new()
|
||||
bottom.add_child(board_zone)
|
||||
board_zone.ap_bar.set_refresh_cost(int(db.action_economy.rap_refresh_cost)) # RAP 读数表值驱动
|
||||
board_zone.ap_bar.set_refresh_cost(int(db.run.action_economy.rap_refresh_cost)) # RAP 读数表值驱动
|
||||
|
||||
# 结算演出覆盖层(A–D 之上——z 序 = addChild 序,模态覆盖层组在 build_overlays 中更晚挂入)
|
||||
var settlement_fx := SettlementFxView.new()
|
||||
@@ -65,7 +65,7 @@ static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary:
|
||||
# 本组不含明细回看 overlay——冒险中回看归「生涯」页;
|
||||
# 本组不含营地覆盖层——在 camp_screen 屏壳构建)
|
||||
static func build_overlays(host: Control, db: ConfigDb) -> Dictionary:
|
||||
var legend_overlay := LegendOverlay.new(int(db.action_economy.rap_refresh_cost)) # RAP 行表值注入
|
||||
var legend_overlay := LegendOverlay.new(int(db.run.action_economy.rap_refresh_cost)) # RAP 行表值注入
|
||||
host.add_child(legend_overlay)
|
||||
|
||||
var node_overlay := NodeOverlay.new()
|
||||
@@ -101,31 +101,11 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar
|
||||
var bd := UiKit.backdrop_tile()
|
||||
if bd != null:
|
||||
overlay.add_child(bd)
|
||||
overlay.add_child(UiKit.beams_rect())
|
||||
overlay.add_child(UiKit._beams_rect())
|
||||
overlay.add_child(UiKit.vignette_rect())
|
||||
# 双列骨架:左=操作列 56% / 右=舞台(立绘看台)
|
||||
var cols := HBoxContainer.new()
|
||||
cols.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
cols.add_theme_constant_override("separation", 32)
|
||||
var pad := MarginContainer.new()
|
||||
pad.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]:
|
||||
pad.add_theme_constant_override(side, 56)
|
||||
cols.add_child(pad)
|
||||
overlay.add_child(cols)
|
||||
var box := VBoxContainer.new()
|
||||
box.name = "start_box"
|
||||
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
box.size_flags_stretch_ratio = 1.4
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
pad.add_child(box)
|
||||
|
||||
var title := UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP)
|
||||
box.add_child(title)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
# 三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1:进入存档/设置/退出游戏;
|
||||
# 组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕)
|
||||
var cols := _start_columns(overlay)
|
||||
var box := _start_left_column(cols)
|
||||
var entersavebtn := UiKit.primary_button("进入存档") # 主操作钮(唯一进度入口)
|
||||
entersavebtn.tooltip_text = "选择一个存档位进入(新档 / 进档 / 删档)"
|
||||
box.add_child(entersavebtn)
|
||||
@@ -135,12 +115,45 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar
|
||||
# 退出游戏与设置同层并行——只在一级菜单出现(行为零变化——契约 2)
|
||||
var quitbtn := UiKit.button("退出游戏")
|
||||
box.add_child(quitbtn)
|
||||
|
||||
# 调试入口收浮层角标;boot 菜单优先不开
|
||||
# 浮窗,调试入口=UI 最顶层全局浮层角标(build_debug_badge——LAYER_CONTRACT 末位、
|
||||
# ModalStack 登记外,任何 UI 不遮挡;本层不设屏内调试角标,全局浮层全场景承载)
|
||||
_start_right_column(cols)
|
||||
return {"overlay": overlay, "box": box,
|
||||
"entersavebtn": entersavebtn,
|
||||
"settingsbtn": settingsbtn, "quitbtn": quitbtn}
|
||||
|
||||
# 右列:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌)
|
||||
|
||||
## 双列骨架域:左=操作列 56% / 右=舞台(立绘看台)——全屏锚+56px 四边衬距
|
||||
static func _start_columns(overlay: Control) -> HBoxContainer:
|
||||
var cols := HBoxContainer.new()
|
||||
cols.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
cols.add_theme_constant_override("separation", 32)
|
||||
var pad := MarginContainer.new()
|
||||
pad.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]:
|
||||
pad.add_theme_constant_override(side, 56)
|
||||
cols.add_child(pad)
|
||||
overlay.add_child(cols)
|
||||
return cols
|
||||
|
||||
|
||||
## 左操作列域:标题+三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1:
|
||||
## 进入存档/设置/退出游戏;组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕)
|
||||
static func _start_left_column(cols: HBoxContainer) -> VBoxContainer:
|
||||
var box := VBoxContainer.new()
|
||||
box.name = "start_box"
|
||||
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
box.size_flags_stretch_ratio = 1.4
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
(cols.get_child(0) as MarginContainer).add_child(box)
|
||||
box.add_child(UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP))
|
||||
box.add_child(HSeparator.new())
|
||||
return box
|
||||
|
||||
|
||||
## 右列域:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌)
|
||||
static func _start_right_column(cols: HBoxContainer) -> void:
|
||||
var right := VBoxContainer.new()
|
||||
right.name = "start_right"
|
||||
right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -153,17 +166,7 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar
|
||||
foe_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
foe_row.add_theme_constant_override("separation", 36)
|
||||
for cfg in [[UiKit.TEX_ENEMY_GOBLIN, 46, 96], [UiKit.TEX_HERO_PORTRAIT, 138, 144], [UiKit.TEX_ENEMY_OGRE_CHIEF, 80, 96]]:
|
||||
var tr := TextureRect.new()
|
||||
var tex := UiKit.load_tex(cfg[0])
|
||||
if tex != null:
|
||||
tr.texture = tex
|
||||
tr.custom_minimum_size = Vector2(cfg[1], cfg[2])
|
||||
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
if cfg[0] != UiKit.TEX_HERO_PORTRAIT:
|
||||
tr.modulate = Color(0.7, 0.7, 0.7, 0.4) # 敌影剪影退后(§6.1 立绘>背景)
|
||||
foe_row.add_child(tr)
|
||||
foe_row.add_child(_start_portrait(cfg))
|
||||
stage.add_child(foe_row)
|
||||
var pedestal := Panel.new()
|
||||
var ped_sb := UiKit.panel_flat(UiKit.TITLE_PEDESTAL, UiKit.BG_ROOT, 17, 2)
|
||||
@@ -173,9 +176,20 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar
|
||||
stage.add_child(pedestal)
|
||||
right.add_child(stage)
|
||||
|
||||
return {"overlay": overlay, "box": box,
|
||||
"entersavebtn": entersavebtn,
|
||||
"settingsbtn": settingsbtn, "quitbtn": quitbtn}
|
||||
|
||||
## 单立绘域(循环体提函数——像素过滤居中;敌影剪影退后 §6.1 立绘>背景)
|
||||
static func _start_portrait(cfg: Array) -> TextureRect:
|
||||
var tr := TextureRect.new()
|
||||
var tex := UiKit.load_tex(cfg[0])
|
||||
if tex != null:
|
||||
tr.texture = tex
|
||||
tr.custom_minimum_size = Vector2(cfg[1], cfg[2])
|
||||
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
if cfg[0] != UiKit.TEX_HERO_PORTRAIT:
|
||||
tr.modulate = Color(0.7, 0.7, 0.7, 0.4)
|
||||
return tr
|
||||
|
||||
|
||||
# 调试浮层入口角标(UI 最顶层浮层,任何 UI 不遮挡)——main 直接子层、
|
||||
|
||||
@@ -41,6 +41,16 @@ func _init() -> void:
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
add_child(row)
|
||||
|
||||
_build_ap_face(row)
|
||||
_build_rap_face(row)
|
||||
_build_refresh_confirm(row)
|
||||
_build_legend_refresh(row)
|
||||
|
||||
|
||||
# ---- 构建私有族(AP 计量/RAP 读数/破坏性确认/图例刷新四域) ----
|
||||
|
||||
# AP 计量域:点阵+读数+停手钮+弹性撑
|
||||
func _build_ap_face(row: HBoxContainer) -> void:
|
||||
_dots = HBoxContainer.new()
|
||||
_dots.add_theme_constant_override("separation", 3)
|
||||
_dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
@@ -48,19 +58,19 @@ func _init() -> void:
|
||||
_ap_label = UiKit.label("AP 0/0",UiKit.FS_BODY, true)
|
||||
_ap_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
row.add_child(_ap_label)
|
||||
|
||||
_btn_stop = UiKit.button("停手")
|
||||
_btn_stop.custom_minimum_size = Vector2(64, 0)
|
||||
_btn_stop.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
_btn_stop.pressed.connect(_emit_stop_pressed)
|
||||
row.add_child(_btn_stop)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(spacer)
|
||||
|
||||
# RAP 读数(自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮
|
||||
# (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则)
|
||||
|
||||
# RAP 读数域(自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮
|
||||
# (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则)
|
||||
func _build_rap_face(row: HBoxContainer) -> void:
|
||||
_rap_dots = HBoxContainer.new()
|
||||
_rap_dots.add_theme_constant_override("separation", 2)
|
||||
_rap_dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
@@ -72,7 +82,9 @@ func _init() -> void:
|
||||
_rap_feedback.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
row.add_child(_rap_feedback)
|
||||
|
||||
# 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位
|
||||
|
||||
# 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位
|
||||
func _build_refresh_confirm(row: HBoxContainer) -> void:
|
||||
_confirm_row = HBoxContainer.new()
|
||||
_confirm_row.add_theme_constant_override("separation", 4)
|
||||
_confirm_row.visible = false
|
||||
@@ -83,12 +95,14 @@ func _init() -> void:
|
||||
confirm_btn.pressed.connect(_disarm_and_fire)
|
||||
_confirm_row.add_child(confirm_btn)
|
||||
|
||||
|
||||
# 图例与弃图刷新域:图例开关钮+刷新钮组(副行=剩余次数读数)
|
||||
func _build_legend_refresh(row: HBoxContainer) -> void:
|
||||
_btn_legend = UiKit.button("图例")
|
||||
_btn_legend.custom_minimum_size = Vector2(56, 0)
|
||||
_btn_legend.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
_btn_legend.pressed.connect(_emit_legend_toggled)
|
||||
row.add_child(_btn_legend)
|
||||
|
||||
var refresh_box := VBoxContainer.new()
|
||||
refresh_box.add_theme_constant_override("separation", 0)
|
||||
refresh_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
|
||||
@@ -10,22 +10,30 @@ const COL_GOLD := UiKit.GOLD
|
||||
const COL_DUSK := UiKit.DUSK # 暮蓝(全作唯一暮蓝档)
|
||||
const COL_WARM := UiKit.WARM
|
||||
|
||||
var _dim: ColorRect
|
||||
var _seal: Panel
|
||||
var _verdict: Label
|
||||
var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏)
|
||||
var _gp_row: HBoxContainer
|
||||
var _gp_name: Label
|
||||
var _gp_val: Label
|
||||
var _ks_row: HBoxContainer
|
||||
var _ks_name: Label
|
||||
var _ks_val: Label
|
||||
var _bonus_row_l: HBoxContainer
|
||||
var _bonus_name: Label
|
||||
var _bonus_val: Label
|
||||
var _cele: TextureRect # 庆祝立绘(idle 占位浮动)
|
||||
var _btn: Button
|
||||
var _board_rect := Rect2()
|
||||
## 内部状态件(u6 子域拆件——契约 9 职责语句「战末呈现与演出态」):单一变化原因=终局画面呈现与
|
||||
## 演出计数。探针直读冻结成员(_btn/_gp_name/_gp_row/_gp_val)住宿主不进状态件。
|
||||
class _EndOverlayState extends RefCounted:
|
||||
var _dim: ColorRect
|
||||
var _seal: Panel
|
||||
var _verdict: Label
|
||||
var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏)
|
||||
var _ks_row: HBoxContainer
|
||||
var _ks_name: Label
|
||||
var _ks_val: Label
|
||||
var _bonus_row_l: HBoxContainer
|
||||
var _bonus_name: Label
|
||||
var _bonus_val: Label
|
||||
var _cele: TextureRect # 庆祝立绘(idle 占位浮动)
|
||||
var _board_rect := Rect2()
|
||||
var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收)
|
||||
var _fx_counts := {"crest": 0, "dust": 0, "seal": 0} # 演出计数(fx_probe 断言面——crest/dust/seal 三键)
|
||||
|
||||
|
||||
var st := _EndOverlayState.new()
|
||||
var _gp_row: HBoxContainer # 探针直读冻结(S17⑤)
|
||||
var _gp_name: Label # 探针直读冻结
|
||||
var _gp_val: Label # 探针直读冻结
|
||||
var _btn: Button # 探针直读冻结
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -33,21 +41,45 @@ func _init() -> void:
|
||||
#(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与
|
||||
# 按钮全部上板;金蜡封 = 面板头部静态徽记(坠落演出落点被面板遮挡)。
|
||||
# 骨架经 UiKit.modal_shell;_dim 引用摘出供 open_* 换氛围色。
|
||||
var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, true,
|
||||
var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12,
|
||||
UiKit.panel_skinned(Vector4(16, 14, 16, 14)))
|
||||
_dim = shell.dim
|
||||
st._dim = shell.dim
|
||||
var box: VBoxContainer = shell.box
|
||||
_build_header(box)
|
||||
_build_ledger(box)
|
||||
# 无明细回看钮——冒险中回看归「生涯」
|
||||
_btn = UiKit.primary_button("继续")
|
||||
_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER # 紧凑钮
|
||||
_btn.custom_minimum_size.x = 150
|
||||
_btn.pressed.connect(_emit_continue_pressed)
|
||||
box.add_child(_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
## 头部域:金蜡封徽记+判定行+庆祝立绘(demo ⑧——idle 帧浮动占位;减动效=静帧)
|
||||
func _build_header(box: VBoxContainer) -> void:
|
||||
var header := HBoxContainer.new()
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
header.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(header)
|
||||
_seal = Panel.new()
|
||||
_seal.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
st._seal = Panel.new()
|
||||
st._seal.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var sb := UiKit.panel_flat(COL_GOLD, UiKit.GOLD_DEEP, 200, 4)
|
||||
_seal.add_theme_stylebox_override("panel", sb)
|
||||
_seal.custom_minimum_size = Vector2(52, 52)
|
||||
_seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
header.add_child(_seal)
|
||||
st._seal.add_theme_stylebox_override("panel", sb)
|
||||
st._seal.custom_minimum_size = Vector2(52, 52)
|
||||
st._seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
header.add_child(st._seal)
|
||||
st._seal.add_child(_seal_img())
|
||||
st._verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
|
||||
st._verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
st._verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
header.add_child(st._verdict)
|
||||
st._cele = _cele_rect()
|
||||
header.add_child(st._cele)
|
||||
|
||||
|
||||
## 蜡封图域(32px 像素过滤居中)
|
||||
func _seal_img() -> TextureRect:
|
||||
var seal_img := TextureRect.new()
|
||||
var seal_tex := UiKit.load_tex("res://assets/art/board/seal_mark.png")
|
||||
if seal_tex != null:
|
||||
@@ -58,76 +90,76 @@ func _init() -> void:
|
||||
seal_img.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
seal_img.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
seal_img.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_seal.add_child(seal_img)
|
||||
_verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
|
||||
_verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
header.add_child(_verdict)
|
||||
# 庆祝立绘(demo ⑧——idle 帧浮动占位;减动效=静帧)
|
||||
_cele = TextureRect.new()
|
||||
return seal_img
|
||||
|
||||
|
||||
## 庆祝立绘域(64px idle 帧)
|
||||
func _cele_rect() -> TextureRect:
|
||||
var cele := TextureRect.new()
|
||||
var cele_tex := UiKit.load_tex(UiKit.TEX_HERO_IDLE)
|
||||
if cele_tex != null:
|
||||
_cele.texture = cele_tex
|
||||
_cele.custom_minimum_size = Vector2(64, 64)
|
||||
_cele.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_cele.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
_cele.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
_cele.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
_cele.visible = false
|
||||
header.add_child(_cele)
|
||||
_rows_box = VBoxContainer.new()
|
||||
_rows_box.add_theme_constant_override("separation", 6)
|
||||
_rows_box.visible = false
|
||||
box.add_child(_rows_box)
|
||||
# 两列对齐账行(审计差距④对齐 demo ⑧——项目名左/数值右,定宽 360)
|
||||
cele.texture = cele_tex
|
||||
cele.custom_minimum_size = Vector2(64, 64)
|
||||
cele.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
cele.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
cele.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
cele.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
cele.visible = false
|
||||
return cele
|
||||
|
||||
|
||||
## 三行账域(两列对齐账行——审计差距④对齐 demo ⑧:项目名左/数值右,定宽 360)
|
||||
func _build_ledger(box: VBoxContainer) -> void:
|
||||
st._rows_box = VBoxContainer.new()
|
||||
st._rows_box.add_theme_constant_override("separation", 6)
|
||||
st._rows_box.visible = false
|
||||
box.add_child(st._rows_box)
|
||||
_gp_row = _ledger_row()
|
||||
_gp_name = _ledger_name(UiKit.FS_SUB, UiKit.GOLD_INK, true) # GOLD_DEEP 对结算板 2.67:1 越带,换深金墨 5.21:1
|
||||
_gp_val = _ledger_value(UiKit.FS_SUB, UiKit.GOLD_INK, true) # 同上
|
||||
_gp_row.add_child(_gp_name)
|
||||
_gp_row.add_child(_gp_val)
|
||||
_rows_box.add_child(_gp_row)
|
||||
_ks_row = _ledger_row()
|
||||
_ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false)
|
||||
_ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true)
|
||||
_ks_row.add_child(_ks_name)
|
||||
_ks_row.add_child(_ks_val)
|
||||
_ks_row.visible = false
|
||||
_rows_box.add_child(_ks_row)
|
||||
_bonus_row_l = _ledger_row()
|
||||
_bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false)
|
||||
_bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true)
|
||||
_bonus_row_l.add_child(_bonus_name)
|
||||
_bonus_row_l.add_child(_bonus_val)
|
||||
_bonus_row_l.visible = false
|
||||
_rows_box.add_child(_bonus_row_l)
|
||||
st._rows_box.add_child(_gp_row)
|
||||
st._ks_row = _ledger_row()
|
||||
st._ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false)
|
||||
st._ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true)
|
||||
st._ks_row.add_child(st._ks_name)
|
||||
st._ks_row.add_child(st._ks_val)
|
||||
st._ks_row.visible = false
|
||||
st._rows_box.add_child(st._ks_row)
|
||||
st._bonus_row_l = _ledger_row()
|
||||
st._bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false)
|
||||
st._bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true)
|
||||
st._bonus_row_l.add_child(st._bonus_name)
|
||||
st._bonus_row_l.add_child(st._bonus_val)
|
||||
st._bonus_row_l.visible = false
|
||||
st._rows_box.add_child(st._bonus_row_l)
|
||||
box.add_child(HSeparator.new())
|
||||
# 无明细回看钮——冒险中回看归「生涯」
|
||||
_btn = UiKit.primary_button("继续")
|
||||
_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER # 紧凑钮
|
||||
_btn.custom_minimum_size.x = 150
|
||||
_btn.pressed.connect(_emit_continue_pressed)
|
||||
box.add_child(_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入。
|
||||
# gp_summary(胜利总结结算展示 GP 总量+增量):组合根装配「战利品 +增量 · 合计总量」
|
||||
# 传入(入账次序 = LevelRunner 入账先于组合根 handler,runner.gp 即战后总量);空串 = 不显示(失败侧恒空)。
|
||||
func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, gp_summary: String = "",
|
||||
ks_summary: String = "", bonus_summary: String = "", gp_gain := -1, gp_total := -1) -> void:
|
||||
_dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo
|
||||
_verdict.text = verdict_line
|
||||
_seal.visible = true # 胜利徽记(面板头部静态)
|
||||
_seal.modulate = Color.WHITE
|
||||
# 可选尾参收拢 opts 束(u6——键 {gp_summary, ks_summary, bonus_summary, gp_gain, gp_total},缺省逐字同值)
|
||||
func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, opts := {}) -> void:
|
||||
var gp_summary: String = opts.get("gp_summary", "")
|
||||
var ks_summary: String = opts.get("ks_summary", "")
|
||||
var bonus_summary: String = opts.get("bonus_summary", "")
|
||||
var gp_gain: int = opts.get("gp_gain", -1)
|
||||
var gp_total: int = opts.get("gp_total", -1)
|
||||
st._dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo
|
||||
st._verdict.text = verdict_line
|
||||
st._seal.visible = true # 胜利徽记(面板头部静态)
|
||||
st._seal.modulate = Color.WHITE
|
||||
_set_ledger_row(_gp_name, _gp_val, gp_summary, true) # GP 行=固定名「战利品 GP」(滚动/整串均只入值列)
|
||||
_set_ledger_row(_ks_name, _ks_val, ks_summary)
|
||||
_ks_row.visible = ks_summary != ""
|
||||
_set_ledger_row(_bonus_name, _bonus_val, bonus_summary)
|
||||
_bonus_row_l.visible = bonus_summary != ""
|
||||
_rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != ""
|
||||
_set_ledger_row(st._ks_name, st._ks_val, ks_summary)
|
||||
st._ks_row.visible = ks_summary != ""
|
||||
_set_ledger_row(st._bonus_name, st._bonus_val, bonus_summary)
|
||||
st._bonus_row_l.visible = bonus_summary != ""
|
||||
st._rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != ""
|
||||
_btn.text = continue_label
|
||||
_board_rect = board_rect
|
||||
_cele.visible = true
|
||||
st._board_rect = board_rect
|
||||
st._cele.visible = true
|
||||
_play_celebrate()
|
||||
visible = true
|
||||
if UiKit.victory_fx_style == "dust":
|
||||
@@ -141,23 +173,23 @@ func open_victory(verdict_line: String, continue_label: String, board_rect: Rect
|
||||
|
||||
func open_defeat(verdict_line: String, continue_label: String, board_rect: Rect2) -> void:
|
||||
# 明度不变的暮蓝色相氛围(全屏唯一暮蓝档——§2.5 最静一拍)
|
||||
_dim.color = Color(COL_DUSK, 0.6) # 同族压暗
|
||||
_verdict.text = verdict_line
|
||||
_board_rect = board_rect
|
||||
st._dim.color = Color(COL_DUSK, 0.6) # 同族压暗
|
||||
st._verdict.text = verdict_line
|
||||
st._board_rect = board_rect
|
||||
visible = true
|
||||
_seal.visible = true
|
||||
_seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧)
|
||||
_cele.visible = false
|
||||
_rows_box.visible = false # 失败侧不呈现账行
|
||||
st._seal.visible = true
|
||||
st._seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧)
|
||||
st._cele.visible = false
|
||||
st._rows_box.visible = false # 失败侧不呈现账行
|
||||
_play_dust_sadle() # 坐砖掸灰(§2.5——终稿)
|
||||
_play_seal_recall(8)
|
||||
|
||||
|
||||
func close() -> void:
|
||||
visible = false
|
||||
_seal.visible = false
|
||||
_seal.modulate = Color.WHITE
|
||||
_cele.visible = false
|
||||
st._seal.visible = false
|
||||
st._seal.modulate = Color.WHITE
|
||||
st._cele.visible = false
|
||||
var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀)
|
||||
if tw != null and tw.is_valid():
|
||||
tw.kill()
|
||||
@@ -170,7 +202,7 @@ func is_open() -> bool:
|
||||
|
||||
|
||||
func verdict_text() -> String:
|
||||
return _verdict.text
|
||||
return st._verdict.text
|
||||
|
||||
|
||||
# 两列账行工厂(审计差距④——名左/值右,定宽 360)
|
||||
@@ -217,68 +249,78 @@ func _fx_center() -> Vector2:
|
||||
|
||||
# 封印化光点收回(失败——插过的封印逐面化光点收回占位,§2.5)
|
||||
func _play_seal_recall(count: int) -> void:
|
||||
_fx_seal_dots = count
|
||||
st._fx_counts["seal"] = count
|
||||
var base := st._board_rect.get_center() if st._board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5
|
||||
for i in count:
|
||||
var dot := ColorRect.new()
|
||||
dot.color = COL_GOLD
|
||||
dot.size = Vector2(6, 6)
|
||||
var base := _board_rect.get_center() if _board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5
|
||||
dot.position = base + Vector2((float(i % 5) - 2.0) * _board_rect.size.x * 0.14 if _board_rect.size.x > 0 else 0.0,
|
||||
(float(i % 3) - 1.0) * _board_rect.size.y * 0.18) # 确定性散布(S2 禁全局随机——占位演出同守)
|
||||
dot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(dot)
|
||||
var tw := dot.create_tween()
|
||||
tw.tween_interval(UiKit.motion(0.1 * i))
|
||||
tw.tween_property(dot, "position:y", dot.position.y - 120.0, UiKit.motion(0.8))
|
||||
tw.parallel().tween_property(dot, "modulate:a", 0.0, UiKit.motion(0.8))
|
||||
tw.tween_callback(dot.queue_free)
|
||||
_spawn_seal_dot(i, base)
|
||||
|
||||
|
||||
# ---- 演出密度(程序化 tween,确定性散布,零新帧资产) ----
|
||||
## 单光点域(循环体提函数——延迟升空渐隐回收;确定性散布 S2 禁全局随机)
|
||||
func _spawn_seal_dot(i: int, base: Vector2) -> void:
|
||||
var dot := ColorRect.new()
|
||||
dot.color = COL_GOLD
|
||||
dot.size = Vector2(6, 6)
|
||||
dot.position = base + Vector2((float(i % 5) - 2.0) * st._board_rect.size.x * 0.14 if st._board_rect.size.x > 0 else 0.0,
|
||||
(float(i % 3) - 1.0) * st._board_rect.size.y * 0.18)
|
||||
dot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(dot)
|
||||
var tw := dot.create_tween()
|
||||
tw.tween_interval(UiKit.motion(0.1 * i))
|
||||
tw.tween_property(dot, "position:y", dot.position.y - 120.0, UiKit.motion(0.8))
|
||||
tw.parallel().tween_property(dot, "modulate:a", 0.0, UiKit.motion(0.8))
|
||||
tw.tween_callback(dot.queue_free)
|
||||
|
||||
var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收)
|
||||
var _fx_crest_shards := 0
|
||||
var _fx_dust_motes := 0
|
||||
var _fx_seal_dots := 0
|
||||
|
||||
# ---- 演出密度(程序化 tween,确定性散布,零新帧资产;计数与碎砖驻留面在 _EndOverlayState) ----
|
||||
|
||||
|
||||
# 碎砖拼纹章(胜利 §2.4——蜡封落定后十枚碎砖自四周向蜡封外环收拢拼成纹章环,驻留至 close)
|
||||
func _play_crest_assemble() -> void:
|
||||
_clear_crest_bricks()
|
||||
_fx_crest_shards = 10
|
||||
st._fx_counts["crest"] = 10
|
||||
var c := _fx_center()
|
||||
for i in 10:
|
||||
var brick := ColorRect.new()
|
||||
brick.color = UiKit.BRICK if i % 2 == 0 else UiKit.FLOOR # 碎砖=砖面族色(掩尘/裸土——图底同源)
|
||||
brick.size = Vector2(12, 7)
|
||||
brick.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var ang := TAU * float(i) / 10.0
|
||||
var ring := c + Vector2(cos(ang), sin(ang)) * 64.0
|
||||
brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场
|
||||
add_child(brick)
|
||||
_crest_bricks.append(brick)
|
||||
var btw := brick.create_tween()
|
||||
btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段)
|
||||
btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
btw.parallel().tween_property(brick, "rotation", sin(ang) * 0.35, UiKit.motion(0.4))
|
||||
_spawn_crest_brick(i, c)
|
||||
|
||||
|
||||
## 单碎砖域(循环体提函数——待蜡封落定后自场外收拢入环;碎砖=砖面族色〔掩尘/裸土——图底同源〕)
|
||||
func _spawn_crest_brick(i: int, c: Vector2) -> void:
|
||||
var brick := ColorRect.new()
|
||||
brick.color = UiKit.BRICK if i % 2 == 0 else UiKit.FLOOR
|
||||
brick.size = Vector2(12, 7)
|
||||
brick.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var ang := TAU * float(i) / 10.0
|
||||
var ring := c + Vector2(cos(ang), sin(ang)) * 64.0
|
||||
brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场
|
||||
add_child(brick)
|
||||
st._crest_bricks.append(brick)
|
||||
var btw := brick.create_tween()
|
||||
btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段)
|
||||
btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
btw.parallel().tween_property(brick, "rotation", sin(ang) * 0.35, UiKit.motion(0.4))
|
||||
|
||||
|
||||
# 坐砖掸灰(失败 §2.5——暖灰尘絮自棋盘中带缓缓飘落,最静一拍的运动承载)
|
||||
func _play_dust_sadle() -> void:
|
||||
_fx_dust_motes = 8
|
||||
st._fx_counts["dust"] = 8
|
||||
var base := _fx_center()
|
||||
for i in 8:
|
||||
var mote := ColorRect.new()
|
||||
mote.color = Color(UiKit.WARM_FAINT, 0.85)
|
||||
mote.size = Vector2(4, 4)
|
||||
mote.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mote.position = base + Vector2((float(i % 4) - 1.5) * 30.0, -24.0 - 8.0 * float(i / 4))
|
||||
add_child(mote)
|
||||
var mtw := mote.create_tween()
|
||||
mtw.tween_interval(UiKit.motion(0.08 * i))
|
||||
mtw.tween_property(mote, "position:y", mote.position.y + 64.0, UiKit.motion(0.9))
|
||||
mtw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(0.9))
|
||||
mtw.tween_callback(mote.queue_free)
|
||||
_spawn_dust_mote(i, base)
|
||||
|
||||
|
||||
## 单尘絮域(循环体提函数——中带散布缓降渐隐)
|
||||
func _spawn_dust_mote(i: int, base: Vector2) -> void:
|
||||
var mote := ColorRect.new()
|
||||
mote.color = Color(UiKit.WARM_FAINT, 0.85)
|
||||
mote.size = Vector2(4, 4)
|
||||
mote.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mote.position = base + Vector2((float(i % 4) - 1.5) * 30.0, -24.0 - 8.0 * float(i / 4))
|
||||
add_child(mote)
|
||||
var mtw := mote.create_tween()
|
||||
mtw.tween_interval(UiKit.motion(0.08 * i))
|
||||
mtw.tween_property(mote, "position:y", mote.position.y + 64.0, UiKit.motion(0.9))
|
||||
mtw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(0.9))
|
||||
mtw.tween_callback(mote.queue_free)
|
||||
|
||||
|
||||
# 庆祝立绘浮动(idle 占位——UiKit.motion 守减动效;motion_scale<1 静帧)
|
||||
@@ -288,29 +330,34 @@ func _play_celebrate() -> void:
|
||||
var prev: Tween = get_meta(&"celebrate_tw", null)
|
||||
if prev != null and prev.is_valid():
|
||||
prev.kill()
|
||||
var tw := _cele.create_tween()
|
||||
var tw := st._cele.create_tween()
|
||||
set_meta(&"celebrate_tw", tw)
|
||||
tw.set_loops()
|
||||
tw.tween_property(_cele, "position:y", _cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(_cele, "position:y", _cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(st._cele, "position:y", st._cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(st._cele, "position:y", st._cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
|
||||
|
||||
# 尘金粒子(备选演出——金屑自面板上带飘落闪灭;确定性散布守 S2)
|
||||
func _play_victory_dust() -> void:
|
||||
_fx_dust_motes += 10
|
||||
st._fx_counts["dust"] += 10
|
||||
var c := _fx_center()
|
||||
for i in 10:
|
||||
var mote := ColorRect.new()
|
||||
mote.color = UiKit.CANDLE
|
||||
mote.size = Vector2(5, 5)
|
||||
mote.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mote.position = c + Vector2((float(i % 5) - 2.0) * 44.0, -40.0 - 10.0 * float(i / 5))
|
||||
add_child(mote)
|
||||
var tw := mote.create_tween()
|
||||
tw.tween_interval(UiKit.motion(0.12 * i))
|
||||
tw.tween_property(mote, "position:y", mote.position.y + 90.0, UiKit.motion(1.1))
|
||||
tw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(1.1))
|
||||
tw.tween_callback(mote.queue_free)
|
||||
_spawn_victory_mote(i, c)
|
||||
|
||||
|
||||
## 单金屑域(循环体提函数——上带散布飘落闪灭)
|
||||
func _spawn_victory_mote(i: int, c: Vector2) -> void:
|
||||
var mote := ColorRect.new()
|
||||
mote.color = UiKit.CANDLE
|
||||
mote.size = Vector2(5, 5)
|
||||
mote.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mote.position = c + Vector2((float(i % 5) - 2.0) * 44.0, -40.0 - 10.0 * float(i / 5))
|
||||
add_child(mote)
|
||||
var tw := mote.create_tween()
|
||||
tw.tween_interval(UiKit.motion(0.12 * i))
|
||||
tw.tween_property(mote, "position:y", mote.position.y + 90.0, UiKit.motion(1.1))
|
||||
tw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(1.1))
|
||||
tw.tween_callback(mote.queue_free)
|
||||
|
||||
|
||||
# GP 行数字滚动(demo ⑧——0.8s 三次缓出;减动效直出终值)
|
||||
@@ -325,15 +372,15 @@ func _roll_gp(gp_gain: int, gp_total: int) -> void:
|
||||
|
||||
|
||||
func _clear_crest_bricks() -> void:
|
||||
for b in _crest_bricks:
|
||||
for b in st._crest_bricks:
|
||||
if is_instance_valid(b):
|
||||
b.queue_free()
|
||||
_crest_bricks.clear()
|
||||
st._crest_bricks.clear()
|
||||
|
||||
|
||||
# 演出探针出口(fx_probe——胜利纹章/失败掸灰/封印光收回三断言面)
|
||||
func fx_probe() -> Dictionary:
|
||||
return {"crest_shards": _fx_crest_shards, "dust_motes": _fx_dust_motes, "seal_dots": _fx_seal_dots}
|
||||
return {"crest_shards": st._fx_counts["crest"], "dust_motes": st._fx_counts["dust"], "seal_dots": st._fx_counts["seal"]}
|
||||
|
||||
|
||||
func _emit_continue_pressed() -> void:
|
||||
|
||||
@@ -133,7 +133,7 @@ func _flush_pending_agg() -> void:
|
||||
func _source_label(src_key: String) -> String:
|
||||
if src_key == "":
|
||||
return ""
|
||||
return String(_db.settlement.core.source_label_map.get(src_key, src_key))
|
||||
return String(_db.combat.settlement.core.source_label_map.get(src_key, src_key))
|
||||
|
||||
|
||||
func _gold_frame(text: String) -> String:
|
||||
@@ -145,71 +145,122 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
|
||||
var src := _source_label(String(row.get("source_class", "")))
|
||||
var round_no: int = int(row.get("round_index", 0))
|
||||
var prefix := " [color=#%s]R%d%s[/color] " % [UiKit.HEX_LOG_ROUND, round_no, ("·" + src) if src != "" else ""]
|
||||
var hl := String(row.get("highlight", ""))
|
||||
var v: int = int(row.get("value", 0))
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
var target := String(row.get("target", ""))
|
||||
var tail := " → %s" % target if target != "" else ""
|
||||
var suffix := "(%s)" % src if src != "" else ""
|
||||
match String(row.get("type", "")):
|
||||
var kind := String(row.get("type", ""))
|
||||
var body := ""
|
||||
match kind:
|
||||
"effect":
|
||||
# GP 型效果行:「GP 袋 +3」兑付反馈——value = 本行 ±power,
|
||||
# escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账)
|
||||
if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]:
|
||||
var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS
|
||||
var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix]
|
||||
return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body])
|
||||
# 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则)
|
||||
var pool: int = int(row.get("pool_value", 0))
|
||||
var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS
|
||||
var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix]
|
||||
return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body])
|
||||
body = _fmt_effect(row, prefix, suffix)
|
||||
"damage":
|
||||
# 伤害行无行上拆行文案——行上归因读数只剩掷点档位随行
|
||||
# 护甲等级掷点留档(掷点读数与档位随行可读;吃掷点分量恒带,棋盘来源行不带)
|
||||
var roll_txt := ""
|
||||
if row.get("roll", null) != null:
|
||||
roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)]
|
||||
if bool(row.get("crit", false)):
|
||||
roll_txt += " · 暴击×%d" % int(_db.settlement.damage_roll.crit_multiplier)
|
||||
elif int(row.tier) < 100:
|
||||
roll_txt += " · ×%d%%" % int(row.tier)
|
||||
roll_txt += "〕"
|
||||
var kill := " ⚔击杀!" if row.get("killed", false) else ""
|
||||
# 伤害数字不明显——伤害值放大加色独立于行文(朱红大一号——数字即信息主角,非同字号色温微差)
|
||||
var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix]
|
||||
var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY)
|
||||
if bool(row.get("crit", false)):
|
||||
return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行
|
||||
return prefix + "[color=%s]%s[/color]" % [body_col, body]
|
||||
"heal":
|
||||
return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, v, tail, suffix]
|
||||
body = _fmt_damage(row, prefix, suffix)
|
||||
"heal", "cast", "decay":
|
||||
body = _fmt_simple(kind, row, prefix, suffix)
|
||||
"reward_ineffective":
|
||||
# 未生效类行:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数
|
||||
var n := (" ×%d" % agg_count) if agg_count > 1 else ""
|
||||
return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix]
|
||||
body = _fmt_ineffective(row, prefix, suffix, agg_count)
|
||||
"seal_miss":
|
||||
var miss_note := ""
|
||||
if hl == "perfect_broken":
|
||||
miss_note = "(本图完美封印已破——清图奖励不再加倍)"
|
||||
elif hl == "":
|
||||
miss_note = "(首次封印落空已豁免——不计入完美封印判定)"
|
||||
var body := "✗ %s%s" % [name_, miss_note]
|
||||
return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body])
|
||||
body = _fmt_seal_miss(row, prefix)
|
||||
"verdict":
|
||||
body = _fmt_verdict(row)
|
||||
_:
|
||||
body = _fmt_default(row, prefix, suffix)
|
||||
return body
|
||||
|
||||
|
||||
## 简单行域(heal/cast/decay 三型——单行单墨直排)
|
||||
func _fmt_simple(kind: String, row: Dictionary, prefix: String, suffix: String) -> String:
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
match kind:
|
||||
"heal":
|
||||
return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, int(row.get("value", 0)), _tail_of(row), suffix]
|
||||
"cast":
|
||||
# 施放行:增益技 0 伤害施放,具名技能
|
||||
return prefix + "[color=#%s]✦ %s%s[/color]" % [UiKit.HEX_LOG_CAST, name_, suffix]
|
||||
"decay":
|
||||
return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, v]
|
||||
"verdict":
|
||||
if int(row.value) == 1:
|
||||
return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_]
|
||||
return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_]
|
||||
"highlight":
|
||||
return prefix + _gold_frame("★ %s" % name_)
|
||||
return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, int(row.get("value", 0))]
|
||||
return ""
|
||||
|
||||
|
||||
## 默认行域(未知型/高亮星标——未知型=名行直排)
|
||||
func _fmt_default(row: Dictionary, prefix: String, suffix: String) -> String:
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
if String(row.get("type", "")) == "highlight":
|
||||
return prefix + _gold_frame("★ %s" % name_)
|
||||
return prefix + name_ + suffix
|
||||
|
||||
|
||||
## 尾注域:目标格指向(行文附加)
|
||||
func _tail_of(row: Dictionary) -> String:
|
||||
var target := String(row.get("target", ""))
|
||||
return " → %s" % target if target != "" else ""
|
||||
|
||||
|
||||
## GP 型效果行域:「GP 袋 +3」兑付反馈——value = 本行 ±power,
|
||||
## escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账)
|
||||
func _fmt_effect(row: Dictionary, prefix: String, suffix: String) -> String:
|
||||
var v: int = int(row.get("value", 0))
|
||||
var hl := String(row.get("highlight", ""))
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]:
|
||||
var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS
|
||||
var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix]
|
||||
return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body])
|
||||
# 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则)
|
||||
var pool: int = int(row.get("pool_value", 0))
|
||||
var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS
|
||||
var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix]
|
||||
return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body])
|
||||
|
||||
|
||||
## 伤害行域:行上归因读数只剩掷点档位随行(护甲等级掷点留档——掷点读数与档位随行可读;
|
||||
## 吃掷点分量恒带,棋盘来源行不带)。伤害数字不明显——伤害值放大加色独立于行文(朱红大一号)
|
||||
func _fmt_damage(row: Dictionary, prefix: String, suffix: String) -> String:
|
||||
var v: int = int(row.get("value", 0))
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
var tail := _tail_of(row)
|
||||
var roll_txt := ""
|
||||
if row.get("roll", null) != null:
|
||||
roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)]
|
||||
if bool(row.get("crit", false)):
|
||||
roll_txt += " · 暴击×%d" % int(_db.combat.settlement.damage_roll.crit_multiplier)
|
||||
elif int(row.tier) < 100:
|
||||
roll_txt += " · ×%d%%" % int(row.tier)
|
||||
roll_txt += "〕"
|
||||
var kill := " ⚔击杀!" if row.get("killed", false) else ""
|
||||
var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix]
|
||||
if bool(row.get("crit", false)):
|
||||
return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行
|
||||
var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY)
|
||||
return prefix + "[color=%s]%s[/color]" % [body_col, body]
|
||||
|
||||
|
||||
## 未生效行域:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数
|
||||
func _fmt_ineffective(row: Dictionary, prefix: String, suffix: String, agg_count: int) -> String:
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
var n := (" ×%d" % agg_count) if agg_count > 1 else ""
|
||||
return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix]
|
||||
|
||||
|
||||
## 封印落空行域:完美破/首失豁免两注记分档(金框=破完美/普通墨=豁免)
|
||||
func _fmt_seal_miss(row: Dictionary, prefix: String) -> String:
|
||||
var hl := String(row.get("highlight", ""))
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
var miss_note := ""
|
||||
if hl == "perfect_broken":
|
||||
miss_note = "(本图完美封印已破——清图奖励不再加倍)"
|
||||
elif hl == "":
|
||||
miss_note = "(首次封印落空已豁免——不计入完美封印判定)"
|
||||
var body := "✗ %s%s" % [name_, miss_note]
|
||||
return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body])
|
||||
|
||||
|
||||
## 裁定行域:胜金/负绿双墨(粗体菱形头)
|
||||
func _fmt_verdict(row: Dictionary) -> String:
|
||||
var name_: String = String(row.get("display_name", ""))
|
||||
if int(row.value) == 1:
|
||||
return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_]
|
||||
return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_]
|
||||
|
||||
|
||||
func _strip_bb(bb: String) -> String:
|
||||
var out := ""
|
||||
var in_tag := false
|
||||
|
||||
@@ -26,18 +26,28 @@ var world: Node2D # StageWorld(preload 脚本实例——Node2D 世
|
||||
|
||||
func _init(db_ref) -> void:
|
||||
custom_minimum_size = Vector2(0, 252)
|
||||
# 面板底(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景
|
||||
# =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿)
|
||||
_build_backdrop()
|
||||
_build_world_stage()
|
||||
_build_text_half(db_ref)
|
||||
_build_companion_layer(db_ref)
|
||||
|
||||
|
||||
## 背景域(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景
|
||||
## =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿)
|
||||
func _build_backdrop() -> void:
|
||||
var bg := Panel.new()
|
||||
var sb := UiKit.panel_flat(UiKit.STAGE_BG, UiKit.PANEL_BORDER, 0, 1)
|
||||
bg.add_theme_stylebox_override("panel", sb)
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(bg)
|
||||
_build_beams_layer()
|
||||
|
||||
# 顶光柱氛围层(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收
|
||||
# UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/
|
||||
# 世界站桩之下——层序=bg < 光柱 < 世界)
|
||||
|
||||
## 顶光柱氛围层域(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收
|
||||
## UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/
|
||||
## 世界站桩之下——层序=bg < 光柱 < 世界)
|
||||
func _build_beams_layer() -> void:
|
||||
var beams := Control.new()
|
||||
beams.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
beams.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -45,7 +55,9 @@ func _init(db_ref) -> void:
|
||||
beams.resized.connect(beams.queue_redraw)
|
||||
add_child(beams)
|
||||
|
||||
# ── Node2D 舞台半区(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5)──
|
||||
|
||||
## Node2D 舞台半区域(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5)
|
||||
func _build_world_stage() -> void:
|
||||
_svp_container = SubViewportContainer.new()
|
||||
_svp_container.stretch = true # 容器尺寸 = 视口逻辑分辨率,恒 1:1
|
||||
_svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # 世界零输入(tooltip 归壳层伴随件)
|
||||
@@ -61,7 +73,9 @@ func _init(db_ref) -> void:
|
||||
world = _STAGE_WORLD.new()
|
||||
_viewport.add_child(world)
|
||||
|
||||
# ── Control 文本半区(round_label / stage_center / log_view 原样保留——C1 永久 Control)──
|
||||
|
||||
## Control 文本半区域(round_label / stage_center / log_view 原样保留——C1 永久 Control)
|
||||
func _build_text_half(db_ref) -> void:
|
||||
var vb := VBoxContainer.new()
|
||||
vb.add_theme_constant_override("separation", 4)
|
||||
vb.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -70,8 +84,14 @@ func _init(db_ref) -> void:
|
||||
vb.offset_right = -_CONTENT_MARGINS.size.x
|
||||
vb.offset_bottom = -_CONTENT_MARGINS.size.y
|
||||
add_child(vb)
|
||||
_build_narr_row(vb)
|
||||
_build_center_row(vb)
|
||||
log_view = BattleLogView.new(db_ref)
|
||||
vb.add_child(log_view)
|
||||
|
||||
# M8(UI 落码批):轮次牌 plate9 素牌化(demo ⑦——墨字上亮牌 12.3:1,暗底暖字退役)
|
||||
|
||||
## 轮次牌域(M8 UI 落码批:plate9 素牌化——demo ⑦,墨字上亮牌 12.3:1,暗底暖字退役)
|
||||
func _build_narr_row(vb: VBoxContainer) -> void:
|
||||
var narr_plate := PanelContainer.new()
|
||||
narr_plate.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(12, 4, 12, 4)))
|
||||
narr_plate.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
@@ -80,12 +100,14 @@ func _init(db_ref) -> void:
|
||||
round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
narr_plate.add_child(round_label)
|
||||
|
||||
|
||||
## 中段行域:提示语居中+敌站桩壳层伴随件容器(明细带展开/收起改行高——世界布局随动)
|
||||
func _build_center_row(vb: VBoxContainer) -> void:
|
||||
_row = HBoxContainer.new()
|
||||
_row.add_theme_constant_override("separation", 10)
|
||||
_row.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
vb.add_child(_row)
|
||||
_row.resized.connect(_on_row_resized) # 明细带展开/收起改行高——世界布局随动
|
||||
|
||||
_row.resized.connect(_on_row_resized)
|
||||
var center := CenterContainer.new()
|
||||
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行内只剩文本——鼠标全放行给壳层伴随件
|
||||
@@ -96,21 +118,17 @@ func _init(db_ref) -> void:
|
||||
stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
center_col.add_child(stage_center)
|
||||
_row.add_child(center)
|
||||
|
||||
enemy_box = Control.new() # 站桩壳层伴随件容器(T3 语义改写)
|
||||
enemy_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_row.add_child(enemy_box)
|
||||
|
||||
log_view = BattleLogView.new(db_ref)
|
||||
vb.add_child(log_view)
|
||||
|
||||
# ── 壳层伴随件层(最顶——世界站桩之上的 Control 触达面)──
|
||||
## 壳层伴随件层域(最顶——世界站桩之上的 Control 触达面)+主角站桩注册(世界)+ 状态行伴随件(壳层)
|
||||
func _build_companion_layer(db_ref) -> void:
|
||||
_companion_layer = Control.new()
|
||||
_companion_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_companion_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_companion_layer)
|
||||
|
||||
# 主角站桩本体(世界)+ 状态行伴随件(壳层)
|
||||
hero_standee = _STANDEE_BODY.new("hero")
|
||||
var hero_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT)
|
||||
hero_standee.set_portrait_texture(hero_tex, true) # B 区立绘 48px 档·normal 态朝右(素材缺位 null 回退字形「人」)
|
||||
@@ -230,26 +248,24 @@ func enemy_body_at(i: int) -> Node2D:
|
||||
# 素材装载属组合点职责(主角侧同款先例 _init TEX_HERO_PORTRAIT),StandeeBody 保持纯收 tex 面。
|
||||
# 素材缺位 load_tex→null→set_portrait_texture 字形回退(表现与素材批前逐位一致);未录键返回 null
|
||||
# 走默认字形「敌」(与 StandeeBody._portrait_glyph 默认档同口径)——数据表扩键先补此处映射。
|
||||
## 敌像 id→纹理键映射表(查表+回退——映射语义零变化;未知 id=null 回退字形)
|
||||
func _enemy_portrait_tex(enemy_id: String) -> Texture2D:
|
||||
match enemy_id:
|
||||
"goblin":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_GOBLIN)
|
||||
"bugbear":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_BUGBEAR)
|
||||
"ogre_chief":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_OGRE_CHIEF)
|
||||
"giant_spider":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_GIANT_SPIDER)
|
||||
"drider":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_DRIDER)
|
||||
"duergar":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_DUERGAR)
|
||||
"hook_horror":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_HOOK_HORROR)
|
||||
"duergar_king":
|
||||
return UiKit.load_tex(UiKit.TEX_ENEMY_DUERGAR_KING)
|
||||
_:
|
||||
return null
|
||||
var table := {"goblin": UiKit.TEX_ENEMY_GOBLIN, "bugbear": UiKit.TEX_ENEMY_BUGBEAR,
|
||||
"ogre_chief": UiKit.TEX_ENEMY_OGRE_CHIEF, "giant_spider": UiKit.TEX_ENEMY_GIANT_SPIDER,
|
||||
"drider": UiKit.TEX_ENEMY_DRIDER, "duergar": UiKit.TEX_ENEMY_DUERGAR,
|
||||
"hook_horror": UiKit.TEX_ENEMY_HOOK_HORROR, "duergar_king": UiKit.TEX_ENEMY_DUERGAR_KING}
|
||||
return UiKit.load_tex(String(table.get(enemy_id, ""))) if table.has(enemy_id) else null
|
||||
|
||||
|
||||
## 单敌站桩构建域(循环体提函数——世界本体+壳层伴随件+敌种立绘接线〔仅重建拍装载一次;
|
||||
## flip=false——八敌像素源核证朝左/正面,呈现即朝主角无误;反向朝向源换代时翻转义务归素材批〕)
|
||||
func _spawn_enemy_standee(d: Dictionary) -> Node2D:
|
||||
var body: Node2D = _STANDEE_BODY.new("enemy")
|
||||
world.add_child(body)
|
||||
body.slot_row = _make_slot_companion(enemy_box)
|
||||
body.set_data(d)
|
||||
body.set_portrait_texture(_enemy_portrait_tex(String(d.get("enemy_id", ""))), false)
|
||||
return body
|
||||
|
||||
|
||||
# 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律);
|
||||
@@ -263,14 +279,7 @@ func set_enemies(cards_data: Array) -> void:
|
||||
c.queue_free()
|
||||
_cards.clear()
|
||||
for d in cards_data:
|
||||
var body: Node2D = _STANDEE_BODY.new("enemy")
|
||||
world.add_child(body)
|
||||
body.slot_row = _make_slot_companion(enemy_box)
|
||||
body.set_data(d)
|
||||
# 敌种立绘接线(仅重建拍装载一次;flip=false——八敌像素源核证朝左/正面,
|
||||
# 呈现即朝主角无误;反向朝向源换代时翻转义务归素材批)
|
||||
body.set_portrait_texture(_enemy_portrait_tex(String(d.get("enemy_id", ""))), false)
|
||||
_cards.append(body)
|
||||
_cards.append(_spawn_enemy_standee(d))
|
||||
_sync_layout.call_deferred()
|
||||
return
|
||||
for i in cards_data.size():
|
||||
|
||||
@@ -1,68 +1,13 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。
|
||||
## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。
|
||||
## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳,
|
||||
## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义
|
||||
## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。
|
||||
extends Node2D
|
||||
|
||||
const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd")
|
||||
|
||||
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
|
||||
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
|
||||
set(v):
|
||||
edge = v
|
||||
if _glow_layer != null and is_instance_valid(_glow_layer):
|
||||
_glow_layer.setup(edge, glow_group)
|
||||
queue_redraw()
|
||||
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
|
||||
# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8;
|
||||
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
|
||||
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
|
||||
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
|
||||
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
|
||||
var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二)
|
||||
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件)
|
||||
# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)──
|
||||
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
|
||||
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
|
||||
# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。
|
||||
var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入)
|
||||
set(v):
|
||||
facade_south = v
|
||||
queue_redraw()
|
||||
var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入)
|
||||
set(v):
|
||||
proj_band = v
|
||||
queue_redraw()
|
||||
var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载)
|
||||
set(v):
|
||||
glyph = v
|
||||
queue_redraw()
|
||||
var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝)
|
||||
set(v):
|
||||
glyph_color = v
|
||||
queue_redraw()
|
||||
var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区)
|
||||
set(v):
|
||||
badge_glyph = v
|
||||
if _glow_layer != null and is_instance_valid(_glow_layer):
|
||||
_glow_layer.set_badge_mask(v != "")
|
||||
queue_redraw()
|
||||
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝)
|
||||
set(v):
|
||||
dot_visible = v
|
||||
queue_redraw()
|
||||
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
|
||||
set(v):
|
||||
hl_visible = v
|
||||
queue_redraw()
|
||||
var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红)
|
||||
set(v):
|
||||
hl_color = v
|
||||
queue_redraw()
|
||||
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
|
||||
var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
|
||||
set(v):
|
||||
bevel = v
|
||||
queue_redraw()
|
||||
# ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)──
|
||||
const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素)
|
||||
const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64)
|
||||
@@ -76,235 +21,355 @@ const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收
|
||||
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
|
||||
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
|
||||
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
|
||||
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
|
||||
var tex_base: Texture2D = null # ① 砖面底
|
||||
var tex_mutation: Texture2D = null # ② 变异纹带
|
||||
var tex_glyph: Texture2D = null # ③ 数字
|
||||
var tex_badge: Texture2D = null # ③' 徽章
|
||||
var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark
|
||||
set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示))
|
||||
if tex_seal != v:
|
||||
tex_seal = v
|
||||
queue_redraw()
|
||||
var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入)
|
||||
var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用)
|
||||
|
||||
|
||||
## 单格绘制输入集(被动绘制壳的全部绘制入参——BoardView 直写面;setter 触发宿主重绘语义逐字)
|
||||
class _CellPaintState extends RefCounted:
|
||||
var _owner: Node2D = null
|
||||
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
|
||||
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
|
||||
set(v):
|
||||
edge = v
|
||||
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
|
||||
_owner._glow_layer.setup(edge, glow_group)
|
||||
_owner.queue_redraw()
|
||||
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
|
||||
# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8;
|
||||
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
|
||||
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
|
||||
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
|
||||
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
|
||||
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件)
|
||||
# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)──
|
||||
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
|
||||
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
|
||||
# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。
|
||||
var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入)
|
||||
set(v):
|
||||
facade_south = v
|
||||
_owner.queue_redraw()
|
||||
var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入)
|
||||
set(v):
|
||||
proj_band = v
|
||||
_owner.queue_redraw()
|
||||
var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区)
|
||||
set(v):
|
||||
badge_glyph = v
|
||||
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
|
||||
_owner._glow_layer.set_badge_mask(v != "")
|
||||
_owner.queue_redraw()
|
||||
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝)
|
||||
set(v):
|
||||
dot_visible = v
|
||||
_owner.queue_redraw()
|
||||
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
|
||||
set(v):
|
||||
hl_visible = v
|
||||
_owner.queue_redraw()
|
||||
var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红)
|
||||
set(v):
|
||||
hl_color = v
|
||||
_owner.queue_redraw()
|
||||
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
|
||||
var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
|
||||
set(v):
|
||||
bevel = v
|
||||
_owner.queue_redraw()
|
||||
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
|
||||
var tex_base: Texture2D = null # ① 砖面底
|
||||
var tex_mutation: Texture2D = null # ② 变异纹带
|
||||
var tex_glyph: Texture2D = null # ③ 数字
|
||||
var tex_badge: Texture2D = null # ③' 徽章
|
||||
var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark
|
||||
set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示))
|
||||
if tex_seal != v:
|
||||
tex_seal = v
|
||||
_owner.queue_redraw()
|
||||
var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入)
|
||||
var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用)
|
||||
set(v):
|
||||
facade_strip = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
# 变异角标(程序化,tex_mutation 纹带槽暂留):
|
||||
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
|
||||
# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写):
|
||||
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费);
|
||||
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
|
||||
var treasure_mark := "":
|
||||
set(v):
|
||||
if treasure_mark != v:
|
||||
treasure_mark = v
|
||||
_owner.queue_redraw()
|
||||
var mutation_mark := "":
|
||||
set(v):
|
||||
if mutation_mark != v:
|
||||
mutation_mark = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
|
||||
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
|
||||
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
|
||||
var glow_group := 0:
|
||||
set(v):
|
||||
if glow_group != v:
|
||||
glow_group = v
|
||||
_owner._sync_glow_layer()
|
||||
|
||||
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
|
||||
set(v):
|
||||
removed_dim = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
var st := _CellPaintState.new()
|
||||
var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结)
|
||||
set(v):
|
||||
facade_strip = v
|
||||
glyph = v
|
||||
queue_redraw()
|
||||
|
||||
# 变异角标(程序化,tex_mutation 纹带槽暂留):
|
||||
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
|
||||
# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写):
|
||||
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费);
|
||||
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
|
||||
var treasure_mark := "":
|
||||
var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝;探针直读冻结)
|
||||
set(v):
|
||||
if treasure_mark != v:
|
||||
treasure_mark = v
|
||||
queue_redraw()
|
||||
var mutation_mark := "":
|
||||
set(v):
|
||||
if mutation_mark != v:
|
||||
mutation_mark = v
|
||||
queue_redraw()
|
||||
|
||||
glyph_color = v
|
||||
queue_redraw()
|
||||
var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二;探针直读冻结)
|
||||
# ── 视野迷雾呈现态(纯呈现零逻辑;规格=art-bible §6.5 v3.5,现行事实源=HUD框架「视野迷雾」节)──
|
||||
# 0=正常(灯光区内)/ 1=全黑(区外未开格——现行事实源「区外未开格=全黑不渲染」不透明档)/ 2=记忆态(区外已开格灰显可读)
|
||||
var fog_state := 0:
|
||||
var fog_state := 0: # 探针直读冻结
|
||||
set(v):
|
||||
if fog_state != v:
|
||||
fog_state = v
|
||||
queue_redraw()
|
||||
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
|
||||
set(v):
|
||||
removed_dim = v
|
||||
queue_redraw()
|
||||
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
|
||||
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
|
||||
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
|
||||
var glow_group := 0:
|
||||
set(v):
|
||||
if glow_group != v:
|
||||
glow_group = v
|
||||
_sync_glow_layer()
|
||||
|
||||
|
||||
var _glow_layer: Node2D = null
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
st._owner = self
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, Vector2(edge, edge))
|
||||
# ① 砖面底(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;
|
||||
# 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘)
|
||||
if tex_base != null:
|
||||
draw_texture_rect(tex_base, rect, false)
|
||||
var rect := Rect2(Vector2.ZERO, Vector2(st.edge, st.edge))
|
||||
_draw_base_layer(rect)
|
||||
_draw_mutation_marks()
|
||||
_draw_glyph_layer(rect)
|
||||
_draw_overlay_layers(rect)
|
||||
|
||||
|
||||
## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层;
|
||||
## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化)
|
||||
func _draw_base_layer(rect: Rect2) -> void:
|
||||
if st.tex_base != null:
|
||||
draw_texture_rect(st.tex_base, rect, false)
|
||||
# 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序——
|
||||
# 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚;
|
||||
# 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
if not outline_sides.is_empty():
|
||||
var ow := 2.0
|
||||
var oc := UiKit.FOG_OUTLINE
|
||||
for b in outline_sides:
|
||||
var or_ := Rect2()
|
||||
match int(b):
|
||||
0: or_ = Rect2(0, 0, edge, ow)
|
||||
1: or_ = Rect2(0, edge - ow, edge, ow)
|
||||
2: or_ = Rect2(0, 0, ow, edge)
|
||||
3: or_ = Rect2(edge - ow, 0, ow, edge)
|
||||
draw_rect(or_, oc, true)
|
||||
if not st.outline_sides.is_empty():
|
||||
_draw_outline_sides()
|
||||
_draw_south_facade()
|
||||
_draw_north_projection()
|
||||
_draw_e_facade()
|
||||
# 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb
|
||||
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
|
||||
# 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
|
||||
if bg_sb != null:
|
||||
draw_style_box(bg_sb, rect)
|
||||
if st.bg_sb != null:
|
||||
draw_style_box(st.bg_sb, rect)
|
||||
_draw_contact_or_fallback(rect)
|
||||
|
||||
# 接触带微光(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格):
|
||||
# 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge];
|
||||
# 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮)
|
||||
if contact_col.a > 0.0:
|
||||
|
||||
## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格):
|
||||
## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge];
|
||||
## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮)
|
||||
func _draw_contact_or_fallback(rect: Rect2) -> void:
|
||||
if st.contact_col.a > 0.0:
|
||||
_draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
|
||||
_draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
|
||||
var band := 4.0
|
||||
var half := edge * 0.5
|
||||
for b in contact_bands:
|
||||
var r := Rect2()
|
||||
match int(b):
|
||||
0: r = Rect2(0, 0, edge, band)
|
||||
1: r = Rect2(0, edge - band, edge, band)
|
||||
2: r = Rect2(0, 0, band, edge)
|
||||
3: r = Rect2(edge - band, 0, band, edge)
|
||||
draw_rect(r, contact_col, true)
|
||||
for cc in contact_corners:
|
||||
var pt := Vector2.ZERO
|
||||
match int(cc):
|
||||
0: pt = Vector2(0, 0)
|
||||
1: pt = Vector2(edge, 0)
|
||||
2: pt = Vector2(0, edge)
|
||||
3: pt = Vector2(edge, edge)
|
||||
draw_circle(pt, 6.0, contact_col)
|
||||
draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95)))
|
||||
# 半条边:角点沿两条邻边各延伸 edge/2(坐标推导:横带沿 N/S 自角点向板心侧 half;
|
||||
# NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——正交整边时重叠同色无害)
|
||||
if int(cc) == 0 or int(cc) == 1:
|
||||
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
|
||||
draw_rect(Rect2(pt.x if int(cc) == 0 else pt.x - half, 0.0, half, band), contact_col, true)
|
||||
if int(cc) == 2 or int(cc) == 3:
|
||||
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
|
||||
draw_rect(Rect2(pt.x if int(cc) == 2 else pt.x - half, edge - band, half, band), contact_col, true)
|
||||
if int(cc) == 0 or int(cc) == 2:
|
||||
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
|
||||
draw_rect(Rect2(0.0, pt.y if int(cc) == 0 else pt.y - half, band, half), contact_col, true)
|
||||
if int(cc) == 1 or int(cc) == 3:
|
||||
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
|
||||
draw_rect(Rect2(edge - band, pt.y if int(cc) == 1 else pt.y - half, band, half), contact_col, true)
|
||||
elif bg_sb != null:
|
||||
draw_style_box(bg_sb, rect)
|
||||
_draw_contact_bands()
|
||||
elif st.bg_sb != null:
|
||||
draw_style_box(st.bg_sb, rect)
|
||||
_draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序)
|
||||
_draw_north_projection()
|
||||
# 顶面倒角(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
|
||||
# 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
|
||||
if bevel > 0.0:
|
||||
var base: Color = bg_sb.bg_color
|
||||
var lit: Color = base.lightened(0.22 * bevel)
|
||||
var dark: Color = base.darkened(0.22 * bevel)
|
||||
var t := 2.0
|
||||
draw_rect(Rect2(t, t, edge - t * 2.0, t), lit, true)
|
||||
draw_rect(Rect2(t, t, t, edge - t * 2.0), lit, true)
|
||||
draw_rect(Rect2(t, edge - t * 2.0, edge - t * 2.0, t), dark, true)
|
||||
draw_rect(Rect2(edge - t * 2.0, t, t, edge - t * 2.0), dark, true)
|
||||
_draw_bevel()
|
||||
|
||||
|
||||
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
|
||||
func _draw_glyph_layer(rect: Rect2) -> void:
|
||||
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
|
||||
if tex_mutation != null:
|
||||
draw_texture_rect(tex_mutation, rect, false)
|
||||
# ②' 变异角标:左下角危险红三角(危险红=「开此格有代价」语义位,与雷族同源;
|
||||
# 尺寸随格边比例——灰色可辨辅助=红对砖面明度差,禁喧宾)
|
||||
# ②" 秘宝叠加层:真视格=危险红内缩框(策略纹理——确定决策输入不喧宾)
|
||||
if treasure_mark == "mine_reveal":
|
||||
var inset := edge * 0.14
|
||||
draw_rect(Rect2(inset, inset, edge - inset * 2.0, edge - inset * 2.0),
|
||||
Color(UiKit.RED_MINE, 0.9), false, 2.0)
|
||||
if mutation_mark == "flat_damage":
|
||||
var mw := edge * 0.28
|
||||
var mpts := PackedVector2Array([Vector2(0, edge - mw), Vector2(0, edge), Vector2(mw, edge)])
|
||||
draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85))
|
||||
if st.tex_mutation != null:
|
||||
draw_texture_rect(st.tex_mutation, rect, false)
|
||||
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
|
||||
if glyph != "":
|
||||
# 数字色牌后备路径(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
|
||||
# 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
|
||||
# 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
|
||||
if plate:
|
||||
var pw := edge * 0.60
|
||||
var ph := edge * 0.62
|
||||
var pr := Rect2((edge - pw) * 0.5, (edge - ph) * 0.5, pw, ph)
|
||||
draw_rect(pr, UiKit.DIGIT_PLATE, true)
|
||||
draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
|
||||
if tex_glyph != null:
|
||||
draw_texture_rect(tex_glyph, rect, false)
|
||||
_draw_digit_plate()
|
||||
if st.tex_glyph != null:
|
||||
draw_texture_rect(st.tex_glyph, rect, false)
|
||||
else:
|
||||
_draw_glyph(glyph, glyph_color, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式)
|
||||
if badge_glyph != "":
|
||||
if tex_badge != null:
|
||||
draw_texture_rect(tex_badge, rect, false)
|
||||
_draw_glyph(glyph, glyph_color, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式)
|
||||
if st.badge_glyph != "":
|
||||
if st.tex_badge != null:
|
||||
draw_texture_rect(st.tex_badge, rect, false)
|
||||
else:
|
||||
_draw_badge(badge_glyph)
|
||||
_draw_badge(st.badge_glyph)
|
||||
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
|
||||
if tex_seal != null:
|
||||
draw_texture_rect(tex_seal, rect, false)
|
||||
if st.tex_seal != null:
|
||||
draw_texture_rect(st.tex_seal, rect, false)
|
||||
|
||||
|
||||
## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
|
||||
## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
|
||||
## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
|
||||
func _draw_digit_plate() -> void:
|
||||
var pw := st.edge * 0.60
|
||||
var ph := st.edge * 0.62
|
||||
var pr := Rect2((st.edge - pw) * 0.5, (st.edge - ph) * 0.5, pw, ph)
|
||||
draw_rect(pr, UiKit.DIGIT_PLATE, true)
|
||||
draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
|
||||
|
||||
|
||||
## ⑤⑥⑦ 顶层域:路径点迹 + 焦点高亮 + 视野迷雾 + 中立移除退色
|
||||
func _draw_overlay_layers(rect: Rect2) -> void:
|
||||
# ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)
|
||||
if dot_visible:
|
||||
if st.dot_visible:
|
||||
var ds := 6.0
|
||||
draw_rect(Rect2(Vector2((edge - ds) * 0.5, (edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
|
||||
if hl_visible:
|
||||
draw_rect(Rect2(Vector2((st.edge - ds) * 0.5, (st.edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
|
||||
if st.hl_visible:
|
||||
# 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动)
|
||||
draw_rect(Rect2(1, 1, edge - 2.0, edge - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边
|
||||
# ⑥ 视野迷雾层(最顶遮罩,§6.5 落文):
|
||||
# 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论);
|
||||
# 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1)
|
||||
if fog_state == 1:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0, 0, 0, 1.0), true)
|
||||
elif fog_state == 2:
|
||||
# 记忆态换装(demo 口径「地板纹理 + 灰罩 α.66」:
|
||||
# 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。
|
||||
# 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标;
|
||||
# 测点行值与素材随动,见 ui_contrast_probe 迷雾段))
|
||||
if tex_base != null:
|
||||
draw_texture_rect(tex_base, Rect2(Vector2.ZERO, Vector2(edge, edge)), false)
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true)
|
||||
if glyph != "":
|
||||
_draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
|
||||
draw_rect(Rect2(1, 1, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边
|
||||
_draw_fog_layer()
|
||||
# ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
|
||||
# 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
|
||||
if removed_dim:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0.12, 0.10, 0.09, 0.62), true)
|
||||
if st.removed_dim:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true)
|
||||
|
||||
|
||||
## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文):
|
||||
## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论);
|
||||
## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1)
|
||||
func _draw_fog_layer() -> void:
|
||||
if fog_state == 1:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true)
|
||||
elif fog_state == 2:
|
||||
_draw_fog_memory()
|
||||
|
||||
|
||||
## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
func _draw_outline_sides() -> void:
|
||||
var ow := 2.0
|
||||
var oc := UiKit.FOG_OUTLINE
|
||||
for b in st.outline_sides:
|
||||
var or_ := Rect2()
|
||||
match int(b):
|
||||
0: or_ = Rect2(0, 0, st.edge, ow)
|
||||
1: or_ = Rect2(0, st.edge - ow, st.edge, ow)
|
||||
2: or_ = Rect2(0, 0, ow, st.edge)
|
||||
3: or_ = Rect2(st.edge - ow, 0, ow, st.edge)
|
||||
draw_rect(or_, oc, true)
|
||||
|
||||
|
||||
## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边
|
||||
func _draw_contact_bands() -> void:
|
||||
var band := 4.0
|
||||
var half := st.edge * 0.5
|
||||
for b in st.contact_bands:
|
||||
var r := Rect2()
|
||||
match int(b):
|
||||
0: r = Rect2(0, 0, st.edge, band)
|
||||
1: r = Rect2(0, st.edge - band, st.edge, band)
|
||||
2: r = Rect2(0, 0, band, st.edge)
|
||||
3: r = Rect2(st.edge - band, 0, band, st.edge)
|
||||
draw_rect(r, st.contact_col, true)
|
||||
for cc in st.contact_corners:
|
||||
_draw_contact_corner(int(cc), band, half)
|
||||
|
||||
|
||||
## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2——
|
||||
## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——
|
||||
## 正交整边时重叠同色无害)
|
||||
func _draw_contact_corner(cc: int, band: float, half: float) -> void:
|
||||
var pt := Vector2.ZERO
|
||||
match cc:
|
||||
0: pt = Vector2(0, 0)
|
||||
1: pt = Vector2(st.edge, 0)
|
||||
2: pt = Vector2(0, st.edge)
|
||||
3: pt = Vector2(st.edge, st.edge)
|
||||
draw_circle(pt, 6.0, st.contact_col)
|
||||
draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, st.contact_col.a * 0.95)))
|
||||
if cc == 0 or cc == 1:
|
||||
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
|
||||
draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), st.contact_col, true)
|
||||
if cc == 2 or cc == 3:
|
||||
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
|
||||
draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, st.edge - band, half, band), st.contact_col, true)
|
||||
if cc == 0 or cc == 2:
|
||||
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
|
||||
draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), st.contact_col, true)
|
||||
if cc == 1 or cc == 3:
|
||||
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
|
||||
draw_rect(Rect2(st.edge - band, pt.y if cc == 1 else pt.y - half, band, half), st.contact_col, true)
|
||||
|
||||
|
||||
## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
|
||||
## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
|
||||
func _draw_bevel() -> void:
|
||||
if st.bevel <= 0.0:
|
||||
return
|
||||
var base: Color = st.bg_sb.bg_color
|
||||
var lit: Color = base.lightened(0.22 * st.bevel)
|
||||
var dark: Color = base.darkened(0.22 * st.bevel)
|
||||
var t := 2.0
|
||||
draw_rect(Rect2(t, t, st.edge - t * 2.0, t), lit, true)
|
||||
draw_rect(Rect2(t, t, t, st.edge - t * 2.0), lit, true)
|
||||
draw_rect(Rect2(t, st.edge - t * 2.0, st.edge - t * 2.0, t), dark, true)
|
||||
draw_rect(Rect2(st.edge - t * 2.0, t, t, st.edge - t * 2.0), dark, true)
|
||||
|
||||
|
||||
## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角
|
||||
func _draw_mutation_marks() -> void:
|
||||
if 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))
|
||||
|
||||
|
||||
## 记忆态换装域(demo 口径「地板纹理 + 灰罩 α.66」:
|
||||
## 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。
|
||||
## 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标;
|
||||
## 测点行值与素材随动,见 ui_contrast_probe 迷雾段))
|
||||
func _draw_fog_memory() -> void:
|
||||
if st.tex_base != null:
|
||||
draw_texture_rect(st.tex_base, Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), false)
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true)
|
||||
if glyph != "":
|
||||
_draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
|
||||
|
||||
|
||||
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
|
||||
# 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far],
|
||||
# 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致
|
||||
func _draw_south_facade() -> void:
|
||||
if not facade_south:
|
||||
if not st.facade_south:
|
||||
return
|
||||
var far_h := edge * FACADE_FAR_FRAC
|
||||
var near_h := edge * FACADE_NEAR_FRAC
|
||||
var y_far := edge - far_h
|
||||
var far_h := st.edge * FACADE_FAR_FRAC
|
||||
var near_h := st.edge * FACADE_NEAR_FRAC
|
||||
var y_far := st.edge - far_h
|
||||
var y_near := y_far - near_h
|
||||
var y_hl := y_near - FACADE_HIGHLIGHT_PX
|
||||
draw_rect(Rect2(0.0, y_hl, edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点)
|
||||
draw_rect(Rect2(0.0, y_near, edge, near_h), FACADE_NEAR_COL, true)
|
||||
draw_rect(Rect2(0.0, y_far, edge, far_h), FACADE_FAR_COL, true)
|
||||
draw_rect(Rect2(0.0, y_hl, st.edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点)
|
||||
draw_rect(Rect2(0.0, y_near, st.edge, near_h), FACADE_NEAR_COL, true)
|
||||
draw_rect(Rect2(0.0, y_far, st.edge, far_h), FACADE_FAR_COL, true)
|
||||
|
||||
|
||||
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
|
||||
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
|
||||
func _draw_north_projection() -> void:
|
||||
if proj_band <= 0.0:
|
||||
if st.proj_band <= 0.0:
|
||||
return
|
||||
var seg := proj_band / 3.0
|
||||
draw_rect(Rect2(0.0, 0.0, edge, seg), PROJECTION_NEAR_COL, true)
|
||||
draw_rect(Rect2(0.0, seg, edge, seg), PROJECTION_MID_COL, true)
|
||||
draw_rect(Rect2(0.0, seg * 2.0, edge, seg), PROJECTION_EDGE_COL, true)
|
||||
var seg := st.proj_band / 3.0
|
||||
draw_rect(Rect2(0.0, 0.0, st.edge, seg), PROJECTION_NEAR_COL, true)
|
||||
draw_rect(Rect2(0.0, seg, st.edge, seg), PROJECTION_MID_COL, true)
|
||||
draw_rect(Rect2(0.0, seg * 2.0, st.edge, seg), PROJECTION_EDGE_COL, true)
|
||||
|
||||
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 →
|
||||
@@ -312,31 +377,31 @@ func _draw_north_projection() -> void:
|
||||
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
|
||||
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
|
||||
func _draw_e_facade() -> void:
|
||||
if not facade_strip or tex_facade == null:
|
||||
if not st.facade_strip or st.tex_facade == null:
|
||||
return
|
||||
var strip_h := edge * E_FACADE_FRAC
|
||||
var y0 := edge - strip_h
|
||||
var ts := tex_facade.get_size()
|
||||
draw_texture_rect_region(tex_facade, Rect2(0.0, y0, edge, strip_h),
|
||||
var strip_h := st.edge * E_FACADE_FRAC
|
||||
var y0 := st.edge - strip_h
|
||||
var ts := st.tex_facade.get_size()
|
||||
draw_texture_rect_region(st.tex_facade, Rect2(0.0, y0, st.edge, strip_h),
|
||||
Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC))
|
||||
draw_rect(Rect2(0.0, y0, edge, 1.0), UiKit.E_FACADE_FOOT_COL, true)
|
||||
draw_rect(Rect2(0.0, y0, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true)
|
||||
|
||||
|
||||
# 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部)
|
||||
func _draw_glyph(g: String, col: Color, fs: int) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
var f := st.font if st.font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var asc := f.get_ascent(fs)
|
||||
var desc := f.get_descent(fs)
|
||||
draw_string(f, Vector2(0.0, (edge - (asc + desc)) * 0.5 + asc), g,
|
||||
HORIZONTAL_ALIGNMENT_CENTER, edge, fs, col)
|
||||
draw_string(f, Vector2(0.0, (st.edge - (asc + desc)) * 0.5 + asc), g,
|
||||
HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col)
|
||||
|
||||
|
||||
# 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻,
|
||||
# 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。
|
||||
func _sync_glow_layer() -> void:
|
||||
if glow_group == 0:
|
||||
if st.glow_group == 0:
|
||||
if _glow_layer != null and is_instance_valid(_glow_layer):
|
||||
_glow_layer.queue_free()
|
||||
_glow_layer = null
|
||||
@@ -344,15 +409,15 @@ func _sync_glow_layer() -> void:
|
||||
if _glow_layer == null or not is_instance_valid(_glow_layer):
|
||||
_glow_layer = _GLOW_LAYER.new()
|
||||
add_child(_glow_layer)
|
||||
_glow_layer.setup(edge, glow_group)
|
||||
_glow_layer.set_badge_mask(badge_glyph != "")
|
||||
_glow_layer.setup(st.edge, st.glow_group)
|
||||
_glow_layer.set_badge_mask(st.badge_glyph != "")
|
||||
|
||||
|
||||
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
|
||||
func _draw_badge(g: String) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
var f := st.font if st.font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var fs := maxi(10, int(edge * 0.28))
|
||||
var fs := maxi(10, int(st.edge * 0.28))
|
||||
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
|
||||
draw_string(f, Vector2(edge - sz.x - 2.0, edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
|
||||
draw_string(f, Vector2(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
|
||||
@@ -89,19 +89,24 @@ func play_boss_shadow() -> void:
|
||||
|
||||
# 完美封印:金光带加倍扫射 + 横幅占位(§2.2 最高档)
|
||||
func play_perfect_sweep() -> void:
|
||||
for i in 2:
|
||||
var band := ColorRect.new()
|
||||
band.color = Color(COL_GOLD, 0.35)
|
||||
band.size = Vector2(size.x, 14.0 + 10.0 * i)
|
||||
band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i)
|
||||
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(band)
|
||||
var tw := band.create_tween()
|
||||
tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i))
|
||||
tw.tween_callback(band.queue_free)
|
||||
for i in 3:
|
||||
_spawn_sweep_band(i, size)
|
||||
_show_banner("完美封印 ×2")
|
||||
|
||||
|
||||
## 单光带域(循环体提函数——自左掠过渐出)
|
||||
func _spawn_sweep_band(i: int, size: Vector2) -> void:
|
||||
var band := ColorRect.new()
|
||||
band.color = Color(COL_GOLD, 0.35)
|
||||
band.size = Vector2(size.x, 14.0 + 10.0 * i)
|
||||
band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i)
|
||||
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(band)
|
||||
var tw := band.create_tween()
|
||||
tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i))
|
||||
tw.tween_callback(band.queue_free)
|
||||
|
||||
|
||||
# 封印命中:封印化暴击弹道加重占位(自棋盘上缘飞向 B 区敌人卡方向——越界即收)
|
||||
func play_seal_hit_projectile(heavy: bool) -> void:
|
||||
var shot := ColorRect.new()
|
||||
|
||||
@@ -182,7 +182,7 @@ func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void:
|
||||
func set_world_font(f: Font) -> void:
|
||||
_world.board_font = f
|
||||
for c in _cells:
|
||||
_cells[c].font = f
|
||||
_cells[c].st.font = f
|
||||
_cells[c].queue_redraw()
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ func set_cell_area(avail: Vector2) -> void:
|
||||
var stride := _cell + float(SEP)
|
||||
for c in _cells:
|
||||
var u: Node2D = _cells[c]
|
||||
u.edge = _cell
|
||||
u.st.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
if _world.hero != null and _hero_sprite.last.x >= 0:
|
||||
_place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点)
|
||||
@@ -247,17 +247,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
for x in _board.width:
|
||||
var c := Vector2i(x, y)
|
||||
var u: Node2D = _CELL_UNIT.new()
|
||||
u.cell = c
|
||||
u.edge = _cell
|
||||
u.st.cell = c
|
||||
u.st.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
u.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
|
||||
if _palette.tiles_ok:
|
||||
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
|
||||
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
|
||||
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
|
||||
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
u.font = _world.board_font
|
||||
u.st.font = _world.board_font
|
||||
_world.add_child(u)
|
||||
_cells[c] = u
|
||||
set_cell_area(size)
|
||||
@@ -509,21 +509,21 @@ func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
var seal: bool = _minesweeper.has_seal(c)
|
||||
var adjacent: bool = _diggable.has(c)
|
||||
if revealed:
|
||||
u.hl_visible = true
|
||||
u.hl_color = COL_HL_TARGET # 移动目标格边框(金)
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金)
|
||||
_set_cursor_shape(c, CURSOR_MOVE_SEM)
|
||||
_show_path_preview(c)
|
||||
elif adjacent:
|
||||
u.hl_visible = true
|
||||
u.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||||
_set_cursor_shape(c, CURSOR_DIG)
|
||||
elif seal:
|
||||
u.hl_visible = true
|
||||
u.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
else:
|
||||
u.hl_visible = true
|
||||
u.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
hover_forbidden.emit(c)
|
||||
|
||||
@@ -531,7 +531,7 @@ func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
func _clear_hover_visuals(c: Vector2i) -> void:
|
||||
if not _cells.has(c):
|
||||
return
|
||||
_cells[c].hl_visible = false
|
||||
_cells[c].st.hl_visible = false
|
||||
_hide_path_preview()
|
||||
|
||||
|
||||
@@ -546,12 +546,12 @@ func _show_path_preview(c: Vector2i) -> void:
|
||||
var path: Array[Variant] = _minesweeper.compute_move_path(c)
|
||||
for pc in path:
|
||||
if _cells.has(pc):
|
||||
_cells[pc].dot_visible = true
|
||||
_cells[pc].st.dot_visible = true
|
||||
|
||||
|
||||
func _hide_path_preview() -> void:
|
||||
for c in _cells:
|
||||
_cells[c].dot_visible = false
|
||||
_cells[c].st.dot_visible = false
|
||||
|
||||
|
||||
# ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ----
|
||||
@@ -611,11 +611,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var digit_col := COL_DIGIT
|
||||
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
|
||||
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
|
||||
u.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
if removed:
|
||||
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
|
||||
seal = false
|
||||
u.tex_seal = null
|
||||
u.st.tex_seal = null
|
||||
if revealed:
|
||||
var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面
|
||||
if rec.get("kind", "") == "mine":
|
||||
@@ -662,8 +662,8 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
|
||||
glyph = "人"
|
||||
digit_col = COL_HERO
|
||||
u.tex_base = tile
|
||||
u.bg_sb = sb
|
||||
u.st.tex_base = tile
|
||||
u.st.bg_sb = sb
|
||||
u.glyph = glyph
|
||||
u.glyph_color = digit_col
|
||||
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
|
||||
@@ -673,11 +673,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
|
||||
u.glyph_color = digit_col
|
||||
u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit)
|
||||
u.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||||
u.facade_south = false
|
||||
u.proj_band = 0.0
|
||||
u.tex_facade = null
|
||||
u.facade_strip = false
|
||||
u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||||
u.st.facade_south = false
|
||||
u.st.proj_band = 0.0
|
||||
u.st.tex_facade = null
|
||||
u.st.facade_strip = false
|
||||
# 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见);
|
||||
# 上方 else 分支已是全路径清残留,尾部重置系冗余且有害
|
||||
var south_n: Vector2i = c + Vector2i(0, 1)
|
||||
@@ -687,44 +687,44 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
if not revealed:
|
||||
# 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面
|
||||
#(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画
|
||||
u.facade_south = south_in and _minesweeper.is_revealed(south_n)
|
||||
u.st.facade_south = south_in and _minesweeper.is_revealed(south_n)
|
||||
# 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界)
|
||||
for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]:
|
||||
var n: Vector2i = c + d
|
||||
if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n):
|
||||
var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3))
|
||||
if code == 1 and u.facade_south:
|
||||
if code == 1 and u.st.facade_south:
|
||||
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
|
||||
if code == 0 and _palette.tiles_ok:
|
||||
continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边)
|
||||
u.outline_sides.append(code)
|
||||
u.st.outline_sides.append(code)
|
||||
else:
|
||||
# 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量)
|
||||
if north_in and not _minesweeper.is_revealed(north_n):
|
||||
u.proj_band = u.edge * WALL_PROJECTION_FRAC
|
||||
u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30%
|
||||
# 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义
|
||||
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
|
||||
u.facade_strip = true
|
||||
u.tex_facade = _palette.tiles.get("wall_center")
|
||||
u.st.facade_strip = true
|
||||
u.st.tex_facade = _palette.tiles.get("wall_center")
|
||||
# ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有
|
||||
# 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算)
|
||||
if not revealed:
|
||||
u.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||||
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||||
if _treasure_preview.has(c) and glyph == "":
|
||||
var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null
|
||||
if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))):
|
||||
glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))])
|
||||
digit_col = COL_GOLD
|
||||
u.badge_glyph = glyph
|
||||
u.st.badge_glyph = glyph
|
||||
# ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位);
|
||||
# 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留)
|
||||
if not revealed and _vmark_cb.is_valid():
|
||||
u.mutation_mark = String(_vmark_cb.call(c))
|
||||
u.st.mutation_mark = String(_vmark_cb.call(c))
|
||||
else:
|
||||
u.mutation_mark = ""
|
||||
u.badge_glyph = _update_badge(c)
|
||||
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
u.st.mutation_mark = ""
|
||||
u.st.badge_glyph = _update_badge(c)
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
# 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/
|
||||
# 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。
|
||||
# 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。
|
||||
@@ -734,7 +734,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
u.fog_state = (2 if revealed else 1) if fogged else 0
|
||||
else:
|
||||
u.fog_state = 0
|
||||
u.removed_dim = removed
|
||||
u.st.removed_dim = removed
|
||||
u.queue_redraw()
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@ func _sync_glow_to_units() -> void:
|
||||
for c in touched:
|
||||
var u: Node2D = _cells.get(c)
|
||||
if u != null:
|
||||
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -777,14 +777,14 @@ func _process(delta: float) -> void:
|
||||
if UiKit.motion_scale < 1.0:
|
||||
return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动)
|
||||
var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线)
|
||||
_contact_unit.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
_contact_unit.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
_contact_unit.queue_redraw()
|
||||
|
||||
|
||||
# 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色
|
||||
func _apply_contact_bands() -> void:
|
||||
if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true):
|
||||
_contact_unit.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.st.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.queue_redraw()
|
||||
_contact_unit = null
|
||||
if _board == null or _minesweeper == null or not _dig_glow:
|
||||
@@ -801,19 +801,19 @@ func _apply_contact_bands() -> void:
|
||||
for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]:
|
||||
if _diggable.has(_minesweeper.hero_pos() + d):
|
||||
corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3)))
|
||||
hu.contact_bands = bands
|
||||
hu.contact_corners = corners
|
||||
hu.st.contact_bands = bands
|
||||
hu.st.contact_corners = corners
|
||||
# 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実);
|
||||
# 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色,
|
||||
# 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。
|
||||
var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0)
|
||||
hu.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
hu.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
hu.queue_redraw()
|
||||
|
||||
|
||||
func _reset_breath() -> void:
|
||||
if _contact_unit != null and is_instance_valid(_contact_unit):
|
||||
_contact_unit.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.st.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.queue_redraw()
|
||||
|
||||
|
||||
@@ -888,5 +888,5 @@ func cell_rect_probe(c: Vector2i) -> Rect2:
|
||||
if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0:
|
||||
return Rect2()
|
||||
var k: Vector2 = host.size / vp_size
|
||||
var local := Rect2(u.global_position, Vector2(u.edge, u.edge))
|
||||
var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge))
|
||||
return Rect2(host.position + local.position * k, local.size * k)
|
||||
@@ -23,6 +23,11 @@ func _init() -> void:
|
||||
var vb := VBoxContainer.new()
|
||||
vb.add_theme_constant_override("separation", 4)
|
||||
add_child(vb)
|
||||
_build_views(vb)
|
||||
|
||||
|
||||
## 视图三件域:AP 条+棋盘区(含动效与 toast 叠层)
|
||||
func _build_views(vb: VBoxContainer) -> void:
|
||||
ap_bar = ApBarView.new()
|
||||
vb.add_child(ap_bar)
|
||||
var board_area := Control.new()
|
||||
@@ -31,6 +36,11 @@ func _init() -> void:
|
||||
vb.add_child(board_area)
|
||||
_board_area = board_area
|
||||
board_area.resized.connect(_on_board_area_resized)
|
||||
_build_board_children(board_area, vb)
|
||||
|
||||
|
||||
## 棋盘子件域:视图+动效+toast 三叠层挂接+状态角标+操作提示条(hint 回挂 vb——树序与拆分前一致)
|
||||
func _build_board_children(board_area: Control, vb: VBoxContainer) -> void:
|
||||
board_view = BoardView.new()
|
||||
board_view.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
board_area.add_child(board_view)
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
## 营地覆盖层:树淬炼/锻造工坊/秘宝匣/奖励格图鉴四柜台只读投影,购买与切换意图经信号外发。
|
||||
## 柜台锁定态(钥匙未点亮)开/关零副作用——零写档零报错、锁定行纯文案无可交互购买路径。
|
||||
## 开合与开始层互斥归组合根承接(打开前隐藏开始层)。
|
||||
## 内部状态件(u1 子域拆件——契约 9 职责轴):_TreeState=树淬炼柜台内容容器(单一变化原因=树行板结构);
|
||||
## _ForgeState=锻造工坊与秘宝匣两柜台行板与锁定门(钥匙门同构);_AttuneState=同调装配段节点与钮册;
|
||||
## _SideState=Y 刷新升级行与最近冒险账面。探针直读冻结成员(_balance_label/_tree_buy_btns/
|
||||
## _tree_fork_btns/_workshop_holder/_workshop_rows)住宿主不进状态件。
|
||||
class_name CampOverlay
|
||||
extends Control
|
||||
|
||||
@@ -12,37 +16,54 @@ signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口
|
||||
signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态)
|
||||
signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level)
|
||||
signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock)
|
||||
signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor)
|
||||
signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select)
|
||||
|
||||
# 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit
|
||||
const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND
|
||||
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
|
||||
|
||||
var _balance_label: Label
|
||||
var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮)
|
||||
var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费)
|
||||
var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费)
|
||||
var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮)
|
||||
var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板)
|
||||
var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线)
|
||||
var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线)
|
||||
var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费)
|
||||
var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见)
|
||||
var _ws_lock_l: Label # 秘宝匣柜台锁定态行
|
||||
var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行)
|
||||
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}]
|
||||
var _attune_header_l: Label # 同调装配段(Slots 点亮呈现)
|
||||
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷)
|
||||
var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行)
|
||||
var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账)
|
||||
var _refresh_btn: Button
|
||||
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
|
||||
|
||||
class _TreeState extends RefCounted:
|
||||
var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮)
|
||||
|
||||
|
||||
class _ForgeState extends RefCounted:
|
||||
var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮)
|
||||
var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板)
|
||||
var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线)
|
||||
var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线)
|
||||
var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费)
|
||||
var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见)
|
||||
var _ws_lock_l: Label # 秘宝匣柜台锁定态行
|
||||
|
||||
|
||||
class _AttuneState extends RefCounted:
|
||||
var _attune_header_l: Label # 同调装配段(Slots 点亮呈现)
|
||||
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷)
|
||||
var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行)
|
||||
|
||||
|
||||
class _SideState extends RefCounted:
|
||||
var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账)
|
||||
var _refresh_btn: Button
|
||||
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
|
||||
|
||||
|
||||
var _balance_label: Label # 探针直读冻结(S17①——住宿主)
|
||||
var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费;住宿主)
|
||||
var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费;住宿主)
|
||||
var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行;探针直读冻结——住宿主)
|
||||
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}](探针消费;住宿主)
|
||||
var _tree := _TreeState.new()
|
||||
var _forge := _ForgeState.new()
|
||||
var _attune := _AttuneState.new()
|
||||
var _side := _SideState.new()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# 底=不透明暖亮档(半透 dim 会透出底下棋盘/战斗提示条,故须实底;demo #v-camp=bg-bright 实底,
|
||||
# 遮蔽语义由组合根输入锁兜底)
|
||||
var dim := UiKit.modal_dim(self, UiKit.BG_BRIGHT)
|
||||
var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT)
|
||||
# 石纹弱质感+暗角(demo .tilebg 0.14/.vignette;缺位不挂即回退平色)
|
||||
var bd := UiKit.backdrop_tile()
|
||||
if bd != null:
|
||||
@@ -61,6 +82,29 @@ func _init() -> void:
|
||||
root.add_child(col)
|
||||
|
||||
# ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ──
|
||||
_build_top_bar(col)
|
||||
|
||||
# ── 四柜台并排(程序占位重排)──
|
||||
# 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高)
|
||||
var camp_scroll := ScrollContainer.new()
|
||||
camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
col.add_child(camp_scroll)
|
||||
var mid := HBoxContainer.new()
|
||||
mid.add_theme_constant_override("separation", 14)
|
||||
mid.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
camp_scroll.add_child(mid)
|
||||
_build_tree_counter(mid)
|
||||
_build_forge_counter(mid)
|
||||
_build_workshop_counter(mid)
|
||||
_build_atlas_counter(mid)
|
||||
_build_bottom_bar(col)
|
||||
visible = false
|
||||
|
||||
|
||||
## 顶栏域:铭牌(标题+副题)+钥石徽章
|
||||
func _build_top_bar(col: VBoxContainer) -> void:
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 10)
|
||||
col.add_child(top)
|
||||
@@ -82,82 +126,78 @@ func _init() -> void:
|
||||
top.add_child(badge)
|
||||
_balance_label = badge.value_label
|
||||
|
||||
# ── 四柜台并排(程序占位重排)──
|
||||
# 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高)
|
||||
var camp_scroll := ScrollContainer.new()
|
||||
camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
col.add_child(camp_scroll)
|
||||
var mid := HBoxContainer.new()
|
||||
mid.add_theme_constant_override("separation", 14)
|
||||
mid.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
camp_scroll.add_child(mid)
|
||||
|
||||
# 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮)
|
||||
_tree_holder = VBoxContainer.new()
|
||||
_tree_holder.add_theme_constant_override("separation", 6)
|
||||
## 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮)
|
||||
func _build_tree_counter(mid: HBoxContainer) -> void:
|
||||
_tree._tree_holder = VBoxContainer.new()
|
||||
_tree._tree_holder.add_theme_constant_override("separation", 6)
|
||||
var tree_scroll := ScrollContainer.new()
|
||||
tree_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
tree_scroll.add_child(_tree_holder)
|
||||
tree_scroll.add_child(_tree._tree_holder)
|
||||
var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300)
|
||||
tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
tree_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(tree_panel)
|
||||
|
||||
# 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板)
|
||||
_forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
_forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_forge_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_forge_workbench = VBoxContainer.new()
|
||||
_forge_workbench.add_theme_constant_override("separation", 6)
|
||||
_forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建)
|
||||
_forge_zone_weapon = VBoxContainer.new()
|
||||
_forge_zone_weapon.add_theme_constant_override("separation", 4)
|
||||
_forge_zone_armor = VBoxContainer.new()
|
||||
_forge_zone_armor.add_theme_constant_override("separation", 4)
|
||||
|
||||
## 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板)
|
||||
func _build_forge_counter(mid: HBoxContainer) -> void:
|
||||
_forge._forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_forge._forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
_forge._forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_forge._forge_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_forge._forge_workbench = VBoxContainer.new()
|
||||
_forge._forge_workbench.add_theme_constant_override("separation", 6)
|
||||
_forge._forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建)
|
||||
_forge._forge_zone_weapon = VBoxContainer.new()
|
||||
_forge._forge_zone_weapon.add_theme_constant_override("separation", 4)
|
||||
_forge._forge_zone_armor = VBoxContainer.new()
|
||||
_forge._forge_zone_armor.add_theme_constant_override("separation", 4)
|
||||
var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 同款深金墨(对柜台板 5.21:1)
|
||||
_forge_workbench.add_child(wb_wh)
|
||||
_forge_workbench.add_child(_forge_zone_weapon)
|
||||
_forge._forge_workbench.add_child(wb_wh)
|
||||
_forge._forge_workbench.add_child(_forge._forge_zone_weapon)
|
||||
var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK)
|
||||
_forge_workbench.add_child(wb_ah)
|
||||
_forge_workbench.add_child(_forge_zone_armor)
|
||||
_forge._forge_workbench.add_child(wb_ah)
|
||||
_forge._forge_workbench.add_child(_forge._forge_zone_armor)
|
||||
var forge_holder := VBoxContainer.new()
|
||||
forge_holder.add_theme_constant_override("separation", 6)
|
||||
forge_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(与树/秘宝匣 scroll 336 min 对齐——锁定态仅锁文也撑住栏高)
|
||||
forge_holder.add_child(_forge_lock_l)
|
||||
forge_holder.add_child(_forge_workbench)
|
||||
forge_holder.add_child(_forge._forge_lock_l)
|
||||
forge_holder.add_child(_forge._forge_workbench)
|
||||
var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300)
|
||||
forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
forge_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(forge_panel)
|
||||
|
||||
# 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行)
|
||||
_ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
_ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_ws_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_ws_scroll = ScrollContainer.new()
|
||||
_ws_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
_ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
|
||||
## 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行)
|
||||
func _build_workshop_counter(mid: HBoxContainer) -> void:
|
||||
_forge._ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_forge._ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
_forge._ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_forge._ws_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_forge._ws_scroll = ScrollContainer.new()
|
||||
_forge._ws_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
_forge._ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_forge._ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_workshop_holder = VBoxContainer.new()
|
||||
_workshop_holder.add_theme_constant_override("separation", 6)
|
||||
_ws_scroll.add_child(_workshop_holder)
|
||||
_forge._ws_scroll.add_child(_workshop_holder)
|
||||
var ws_holder := VBoxContainer.new()
|
||||
ws_holder.add_theme_constant_override("separation", 6)
|
||||
ws_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(锁定态 scroll 隐、仅锁文也撑住栏高)
|
||||
ws_holder.add_child(_ws_lock_l)
|
||||
ws_holder.add_child(_ws_scroll)
|
||||
ws_holder.add_child(_forge._ws_lock_l)
|
||||
ws_holder.add_child(_forge._ws_scroll)
|
||||
var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300)
|
||||
ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
ws_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(ws_panel)
|
||||
|
||||
# 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha)
|
||||
|
||||
## 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha)
|
||||
func _build_atlas_counter(mid: HBoxContainer) -> void:
|
||||
var atlas_holder := VBoxContainer.new()
|
||||
atlas_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(图鉴仅锁文也撑住栏高)
|
||||
var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300)
|
||||
@@ -170,7 +210,9 @@ func _init() -> void:
|
||||
lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
atlas_holder.add_child(lock_l)
|
||||
|
||||
# ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ──
|
||||
|
||||
## 底栏域:关闭钮(左)/ 操作注记(右)+ 最近冒险条带
|
||||
func _build_bottom_bar(col: VBoxContainer) -> void:
|
||||
var close_btn := UiKit.button("关闭营地(回开始界面)")
|
||||
close_btn.pressed.connect(_emit_closed)
|
||||
var bottom := HBoxContainer.new()
|
||||
@@ -182,11 +224,10 @@ func _init() -> void:
|
||||
var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
|
||||
bottom.add_child(caption)
|
||||
col.add_child(bottom)
|
||||
_recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
|
||||
_recent_list.clip_text = true
|
||||
_recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
col.add_child(_recent_list)
|
||||
visible = false
|
||||
_side._recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
|
||||
_side._recent_list.clip_text = true
|
||||
_side._recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
col.add_child(_side._recent_list)
|
||||
|
||||
|
||||
# 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白)
|
||||
@@ -220,7 +261,7 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_
|
||||
# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy,
|
||||
# currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}])
|
||||
func _rebuild_tree(data: Dictionary) -> void:
|
||||
for c in _tree_holder.get_children():
|
||||
for c in _tree._tree_holder.get_children():
|
||||
c.queue_free()
|
||||
_tree_buy_btns.clear()
|
||||
_tree_fork_btns.clear()
|
||||
@@ -233,52 +274,62 @@ func _rebuild_tree(data: Dictionary) -> void:
|
||||
if vein != last_vein:
|
||||
last_vein = vein
|
||||
var vh := UiKit.label(String(vein_names.get(vein, vein)),UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1
|
||||
_tree_holder.add_child(vh)
|
||||
var hb := HBoxContainer.new()
|
||||
hb.add_theme_constant_override("separation", 6)
|
||||
var lvl := int(row.get("level", 0))
|
||||
var cap := int(row.get("cap", 1))
|
||||
var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL)
|
||||
name_l.custom_minimum_size = Vector2(150, 0)
|
||||
name_l.clip_text = true
|
||||
var gate := String(row.get("gate_text", ""))
|
||||
if gate != "":
|
||||
name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失)
|
||||
name_l.tooltip_text = "等级门槛:%s" % gate
|
||||
hb.add_child(name_l)
|
||||
_tree._tree_holder.add_child(vh)
|
||||
if String(row.get("kind", "")) == "fork":
|
||||
continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮
|
||||
var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准
|
||||
btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", ""))))
|
||||
btn.disabled = not bool(row.get("can_buy", false))
|
||||
hb.add_child(btn)
|
||||
_tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn})
|
||||
_tree_holder.add_child(hb)
|
||||
_tree._tree_holder.add_child(_tree_row_line(row))
|
||||
for fg in forks:
|
||||
var group := String(fg.get("group", ""))
|
||||
var active := String(fg.get("active_side", ""))
|
||||
var owned := bool(fg.get("owned", false))
|
||||
var fb := HBoxContainer.new()
|
||||
fb.add_theme_constant_override("separation", 6)
|
||||
var state_txt := "未解锁"
|
||||
if owned:
|
||||
state_txt = "激活:%s" % active
|
||||
var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL)
|
||||
fb.add_child(gl)
|
||||
if not owned:
|
||||
var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", ""))))
|
||||
bb.disabled = not bool(fg.get("can_buy", false))
|
||||
fb.add_child(bb)
|
||||
_tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb})
|
||||
else:
|
||||
for side_row in (fg.get("sides", []) as Array):
|
||||
var sd := String(side_row.get("side", ""))
|
||||
var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免
|
||||
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
|
||||
fb.add_child(sb)
|
||||
_tree_fork_btns.append({"group": group, "side": sd, "btn": sb})
|
||||
_tree_holder.add_child(fb)
|
||||
_tree._tree_holder.add_child(_tree_fork_line(fg))
|
||||
|
||||
|
||||
## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁)
|
||||
func _tree_row_line(row: Dictionary) -> HBoxContainer:
|
||||
var hb := HBoxContainer.new()
|
||||
hb.add_theme_constant_override("separation", 6)
|
||||
var lvl := int(row.get("level", 0))
|
||||
var cap := int(row.get("cap", 1))
|
||||
var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL)
|
||||
name_l.custom_minimum_size = Vector2(150, 0)
|
||||
name_l.clip_text = true
|
||||
var gate := String(row.get("gate_text", ""))
|
||||
if gate != "":
|
||||
name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失)
|
||||
name_l.tooltip_text = "等级门槛:%s" % gate
|
||||
hb.add_child(name_l)
|
||||
var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准
|
||||
btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", ""))))
|
||||
btn.disabled = not bool(row.get("can_buy", false))
|
||||
hb.add_child(btn)
|
||||
_tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn})
|
||||
return hb
|
||||
|
||||
|
||||
## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮
|
||||
func _tree_fork_line(fg: Dictionary) -> HBoxContainer:
|
||||
var group := String(fg.get("group", ""))
|
||||
var active := String(fg.get("active_side", ""))
|
||||
var owned := bool(fg.get("owned", false))
|
||||
var fb := HBoxContainer.new()
|
||||
fb.add_theme_constant_override("separation", 6)
|
||||
var state_txt := "未解锁"
|
||||
if owned:
|
||||
state_txt = "激活:%s" % active
|
||||
var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL)
|
||||
fb.add_child(gl)
|
||||
if not owned:
|
||||
var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", ""))))
|
||||
bb.disabled = not bool(fg.get("can_buy", false))
|
||||
fb.add_child(bb)
|
||||
_tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb})
|
||||
else:
|
||||
for side_row in (fg.get("sides", []) as Array):
|
||||
var sd := String(side_row.get("side", ""))
|
||||
var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免
|
||||
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
|
||||
fb.add_child(sb)
|
||||
_tree_fork_btns.append({"group": group, "side": sd, "btn": sb})
|
||||
return fb
|
||||
|
||||
|
||||
# 工作间钥匙门(锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行;
|
||||
@@ -289,86 +340,106 @@ func _refresh_key_gates(data: Dictionary) -> void:
|
||||
lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0))
|
||||
var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1
|
||||
var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1
|
||||
_forge_lock_l.visible = not forge_open
|
||||
_forge_workbench.visible = forge_open
|
||||
_ws_lock_l.visible = not ws_open
|
||||
_ws_scroll.visible = ws_open
|
||||
_forge._forge_lock_l.visible = not forge_open
|
||||
_forge._forge_workbench.visible = forge_open
|
||||
_forge._ws_lock_l.visible = not ws_open
|
||||
_forge._ws_scroll.visible = ws_open
|
||||
|
||||
|
||||
# 锻造工坊行板重建(两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板:
|
||||
# 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression
|
||||
# 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1)
|
||||
func _rebuild_forge(data: Dictionary) -> void:
|
||||
for zone in [_forge_zone_weapon, _forge_zone_armor]:
|
||||
for zone in [_forge._forge_zone_weapon, _forge._forge_zone_armor]:
|
||||
for c in zone.get_children():
|
||||
c.queue_free()
|
||||
_forge_rows.clear()
|
||||
if not _forge_workbench.visible:
|
||||
_forge._forge_rows.clear()
|
||||
if not _forge._forge_workbench.visible:
|
||||
return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续
|
||||
var bal := int(data.get("balance", 0))
|
||||
for row in data.get("forge", []):
|
||||
var rid := String(row.get("route_id", ""))
|
||||
var kind := String(row.get("kind", ""))
|
||||
var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
var initial := bool(row.get("initial_owned", false))
|
||||
var lvl := int(row.get("level", 0))
|
||||
var cap_v := int(row.get("cap", 0))
|
||||
var next_price := int(row.get("next_price", 0))
|
||||
var unlock_price := int(row.get("unlock_price", 0))
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
card.add_child(line)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(104, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var lvl_l := UiKit.label("Lv%d/%d" % [lvl, cap_v], 13, false, UiKit.INK_LABEL)
|
||||
lvl_l.custom_minimum_size = Vector2(44, 0)
|
||||
line.add_child(lvl_l)
|
||||
# 解锁态/解锁通道呈现(并联双通道:练满前一档 or 一次性解锁价直购)
|
||||
var st_text := "已解锁"
|
||||
if not unlocked:
|
||||
if initial:
|
||||
st_text = "未解锁"
|
||||
else:
|
||||
st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), unlock_price]
|
||||
elif lvl >= cap_v:
|
||||
st_text = "已满级"
|
||||
var status_l := UiKit.label(st_text, UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
# 购买钮(已解锁未满级——几何价显示;余额不足禁用)
|
||||
var buy_btn: Button = null
|
||||
if unlocked and lvl < cap_v:
|
||||
buy_btn = UiKit.button("锻造 %d" % next_price, 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(forge_buy_requested.emit.bind(rid))
|
||||
buy_btn.disabled = bal < next_price
|
||||
line.add_child(buy_btn)
|
||||
# 解锁钮(待解锁非初始——一次性解锁价)
|
||||
var unlock_btn: Button = null
|
||||
if not unlocked and not initial:
|
||||
unlock_btn = UiKit.button("解锁 %d" % unlock_price, 0.0) # 行板紧凑钮豁免
|
||||
unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(rid))
|
||||
unlock_btn.disabled = bal < unlock_price
|
||||
line.add_child(unlock_btn)
|
||||
# 选中钮(已解锁——免费切换;当前=禁用态标记)
|
||||
var select_btn: Button = null
|
||||
if unlocked:
|
||||
var sel := bool(row.get("selected", false))
|
||||
select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免
|
||||
select_btn.pressed.connect(forge_select_requested.emit.bind(rid))
|
||||
select_btn.disabled = sel
|
||||
line.add_child(select_btn)
|
||||
zone.add_child(card)
|
||||
_forge_rows.append({"route_id": rid, "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l,
|
||||
"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn,
|
||||
"next_price": next_price, "unlock_price": unlock_price})
|
||||
var zone: VBoxContainer = _forge._forge_zone_weapon if kind == "weapon" else _forge._forge_zone_armor
|
||||
var card := _forge_route_card(row, bal)
|
||||
zone.add_child(card["card"])
|
||||
_forge._forge_rows.append(card)
|
||||
|
||||
|
||||
## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面)
|
||||
func _forge_route_card(row: Dictionary, bal: int) -> Dictionary:
|
||||
var d := _forge_route_scalars(row)
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
card.add_child(line)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(104, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL)
|
||||
lvl_l.custom_minimum_size = Vector2(44, 0)
|
||||
line.add_child(lvl_l)
|
||||
var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
var btns := _forge_route_buttons(d, row, bal, line)
|
||||
return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l,
|
||||
"buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"],
|
||||
"next_price": d["next_price"], "unlock_price": d["unlock_price"]}
|
||||
|
||||
|
||||
## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数
|
||||
func _forge_route_scalars(row: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"rid": String(row.get("route_id", "")),
|
||||
"unlocked": bool(row.get("unlocked", false)),
|
||||
"initial": bool(row.get("initial_owned", false)),
|
||||
"lvl": int(row.get("level", 0)),
|
||||
"cap_v": int(row.get("cap", 0)),
|
||||
"next_price": int(row.get("next_price", 0)),
|
||||
"unlock_price": int(row.get("unlock_price", 0)),
|
||||
}
|
||||
|
||||
|
||||
## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态)
|
||||
func _forge_status_text(d: Dictionary, row: Dictionary) -> String:
|
||||
var st_text := "已解锁"
|
||||
if not d["unlocked"]:
|
||||
if d["initial"]:
|
||||
st_text = "未解锁"
|
||||
else:
|
||||
st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]]
|
||||
elif d["lvl"] >= d["cap_v"]:
|
||||
st_text = "已满级"
|
||||
return st_text
|
||||
|
||||
|
||||
## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记)
|
||||
func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary:
|
||||
var buy_btn: Button = null
|
||||
if d["unlocked"] and d["lvl"] < d["cap_v"]:
|
||||
buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"]))
|
||||
buy_btn.disabled = bal < d["next_price"]
|
||||
line.add_child(buy_btn)
|
||||
var unlock_btn: Button = null
|
||||
if not d["unlocked"] and not d["initial"]:
|
||||
unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免
|
||||
unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"]))
|
||||
unlock_btn.disabled = bal < d["unlock_price"]
|
||||
line.add_child(unlock_btn)
|
||||
var select_btn: Button = null
|
||||
if d["unlocked"]:
|
||||
var sel := bool(row.get("selected", false))
|
||||
select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免
|
||||
select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"]))
|
||||
select_btn.disabled = sel
|
||||
line.add_child(select_btn)
|
||||
return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn}
|
||||
|
||||
|
||||
# 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建——
|
||||
@@ -389,6 +460,12 @@ func refresh_camp(data: Dictionary) -> void:
|
||||
_rebuild_tree(data)
|
||||
_refresh_key_gates(data)
|
||||
_rebuild_forge(data) # 行板随刷重建(购买/解锁/选中后等级/价/禁用态同步)
|
||||
_refresh_workshop_rows(data, balance)
|
||||
_refresh_attune_segment(data, balance)
|
||||
|
||||
|
||||
## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动
|
||||
func _refresh_workshop_rows(data: Dictionary, balance: int) -> void:
|
||||
var ws_by_id := {}
|
||||
for row in data.get("workshop", []):
|
||||
ws_by_id[String(row.get("item_id", ""))] = row
|
||||
@@ -397,21 +474,7 @@ func refresh_camp(data: Dictionary) -> void:
|
||||
if row.is_empty():
|
||||
continue
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
# 秘宝匣进度列(解锁混合制三形态:钥石购买价/条件进度拼接/已入手)
|
||||
var prog_text := ""
|
||||
var mode := String(row.get("mode", ""))
|
||||
if unlocked:
|
||||
prog_text = "已入手"
|
||||
elif mode == "keystone":
|
||||
prog_text = "钥石 %d" % int(row.get("price", 0))
|
||||
elif mode == "condition":
|
||||
var parts: Array[String] = []
|
||||
for req_v in (row.get("reqs", []) as Array):
|
||||
var req: Dictionary[Variant, Variant] = req_v as Dictionary
|
||||
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
|
||||
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
|
||||
prog_text = "、".join(parts)
|
||||
card.progress_l.text = prog_text
|
||||
card.progress_l.text = _workshop_progress_text(row, unlocked)
|
||||
# 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读)
|
||||
var st_text := "未解锁"
|
||||
var st_col: Color = UiKit.STONE_DEEP
|
||||
@@ -424,111 +487,148 @@ func refresh_camp(data: Dictionary) -> void:
|
||||
# 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter)
|
||||
if card.get("buy_btn") != null:
|
||||
card.buy_btn.disabled = balance < int(card.get("price", 0))
|
||||
# 同调装配段状态就地刷(Slots 点亮/满位禁选/Y 升级行余额联动)
|
||||
|
||||
|
||||
## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手)
|
||||
func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String:
|
||||
if unlocked:
|
||||
return "已入手"
|
||||
var mode := String(row.get("mode", ""))
|
||||
if mode == "keystone":
|
||||
return "钥石 %d" % int(row.get("price", 0))
|
||||
if mode == "condition":
|
||||
var parts: Array[String] = []
|
||||
for req_v in (row.get("reqs", []) as Array):
|
||||
var req: Dictionary[Variant, Variant] = req_v as Dictionary
|
||||
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
|
||||
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
|
||||
return "、".join(parts)
|
||||
return ""
|
||||
|
||||
|
||||
## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷)
|
||||
func _refresh_attune_segment(data: Dictionary, balance: int) -> void:
|
||||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||||
if _attune_header_l != null:
|
||||
if _attune._attune_header_l != null:
|
||||
var slots_n := int(att.get("slots", 0))
|
||||
var eq: Array[Variant] = att.get("equipped", [])
|
||||
_attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n]
|
||||
_attune._attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n]
|
||||
var eq_set := {}
|
||||
for v in eq:
|
||||
eq_set[String(v)] = true
|
||||
for card2 in _attune_btns:
|
||||
for card2 in _attune._attune_btns:
|
||||
var on: bool = eq_set.has(String(card2.item_id))
|
||||
card2.btn.text = "卸下" if on else "装配"
|
||||
# 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新)
|
||||
card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false)))
|
||||
if _refresh_l != null:
|
||||
if _side._refresh_l != null:
|
||||
var rf: Dictionary[Variant, Variant] = att.get("refresh", {})
|
||||
_refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))]
|
||||
_refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0))
|
||||
_refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
|
||||
_side._refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))]
|
||||
_side._refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0))
|
||||
_side._refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
|
||||
|
||||
|
||||
func _rebuild_workshop(data: Dictionary) -> void:
|
||||
_workshop_rows.clear()
|
||||
for child in _workshop_holder.get_children():
|
||||
child.queue_free()
|
||||
_attune_nodes.clear()
|
||||
_attune_btns.clear()
|
||||
_attune_header_l = null
|
||||
_refresh_l = null
|
||||
_refresh_btn = null
|
||||
_attune._attune_nodes.clear()
|
||||
_attune._attune_btns.clear()
|
||||
_attune._attune_header_l = null
|
||||
_side._refresh_l = null
|
||||
_side._refresh_btn = null
|
||||
for row in data.get("workshop", []):
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 12)
|
||||
card.add_child(line)
|
||||
var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), ""))
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(132, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀)
|
||||
line.add_child(name_l)
|
||||
var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案
|
||||
progress_l.custom_minimum_size = Vector2(150, 0)
|
||||
progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜
|
||||
progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(progress_l)
|
||||
# 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式
|
||||
var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者)
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
# 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock,
|
||||
# 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足)
|
||||
var buy_btn: Button = null
|
||||
if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone":
|
||||
buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", ""))))
|
||||
line.add_child(buy_btn)
|
||||
_workshop_holder.add_child(card)
|
||||
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))})
|
||||
var card := _workshop_card(row)
|
||||
_workshop_holder.add_child(card["card"])
|
||||
_workshop_rows.append(card)
|
||||
_rebuild_attune(data)
|
||||
|
||||
|
||||
## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列)
|
||||
func _workshop_card(row: Dictionary) -> Dictionary:
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 12)
|
||||
card.add_child(line)
|
||||
var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), ""))
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(132, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀)
|
||||
line.add_child(name_l)
|
||||
var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案
|
||||
progress_l.custom_minimum_size = Vector2(150, 0)
|
||||
progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜
|
||||
progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(progress_l)
|
||||
# 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式
|
||||
var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者)
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
# 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock,
|
||||
# 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足)
|
||||
var buy_btn: Button = null
|
||||
if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone":
|
||||
buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", ""))))
|
||||
line.add_child(buy_btn)
|
||||
return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card}
|
||||
|
||||
|
||||
# 同调装配段重建(秘宝匣柜台尾部段;Slots 点亮+
|
||||
# 九件行板+Y 升级行,程序占位面)
|
||||
func _rebuild_attune(data: Dictionary) -> void:
|
||||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||||
_attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK)
|
||||
_workshop_holder.add_child(_attune_header_l)
|
||||
_attune_nodes.append(_attune_header_l)
|
||||
_attune._attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK)
|
||||
_workshop_holder.add_child(_attune._attune_header_l)
|
||||
_attune._attune_nodes.append(_attune._attune_header_l)
|
||||
for row in att.get("items", []):
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(96, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
form_l.custom_minimum_size = Vector2(58, 0)
|
||||
line.add_child(form_l)
|
||||
# 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边)
|
||||
var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
tier_l.custom_minimum_size = Vector2(36, 0)
|
||||
line.add_child(tier_l)
|
||||
var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免
|
||||
btn.pressed.connect(attune_toggled.emit.bind(iid))
|
||||
btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫)
|
||||
line.add_child(btn)
|
||||
var line := _attune_item_line(row)
|
||||
_workshop_holder.add_child(line)
|
||||
_attune_nodes.append(line)
|
||||
_attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))})
|
||||
_attune._attune_nodes.append(line)
|
||||
_refresh_attune_tail(att)
|
||||
|
||||
|
||||
## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫)
|
||||
func _attune_item_line(row: Dictionary) -> HBoxContainer:
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(96, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
form_l.custom_minimum_size = Vector2(58, 0)
|
||||
line.add_child(form_l)
|
||||
# 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边)
|
||||
var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
tier_l.custom_minimum_size = Vector2(36, 0)
|
||||
line.add_child(tier_l)
|
||||
var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免
|
||||
btn.pressed.connect(attune_toggled.emit.bind(iid))
|
||||
btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫)
|
||||
line.add_child(btn)
|
||||
_attune._attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))})
|
||||
return line
|
||||
|
||||
|
||||
## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账)
|
||||
func _refresh_attune_tail(att: Dictionary) -> void:
|
||||
var refresh_line := HBoxContainer.new()
|
||||
refresh_line.add_theme_constant_override("separation", 6)
|
||||
_refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
refresh_line.add_child(_refresh_l)
|
||||
_refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免
|
||||
_refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh"))
|
||||
refresh_line.add_child(_refresh_btn)
|
||||
_side._refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
refresh_line.add_child(_side._refresh_l)
|
||||
_side._refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免
|
||||
_side._refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh"))
|
||||
refresh_line.add_child(_side._refresh_btn)
|
||||
_workshop_holder.add_child(refresh_line)
|
||||
_attune_nodes.append(refresh_line)
|
||||
_attune._attune_nodes.append(refresh_line)
|
||||
|
||||
|
||||
func _rebuild_recent(data: Dictionary) -> void:
|
||||
@@ -544,7 +644,7 @@ func _rebuild_recent(data: Dictionary) -> void:
|
||||
verdict = "放弃"
|
||||
lines.append("最近冒险:%s · 完成 %d 关 · 钥石 +%d · 种子 %d" % [verdict,
|
||||
int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))])
|
||||
_recent_list.text = " | ".join(lines)
|
||||
_side._recent_list.text = " | ".join(lines)
|
||||
|
||||
|
||||
func _emit_closed() -> void:
|
||||
|
||||
@@ -14,7 +14,7 @@ var _cancel_btn: Button
|
||||
|
||||
func _init() -> void:
|
||||
var box: VBoxContainer = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(460, 0), 12,
|
||||
true, UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box
|
||||
UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box
|
||||
_text = UiKit.label("",UiKit.FS_BODY, false, UiKit.INK_TITLE)
|
||||
_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_text.custom_minimum_size = Vector2(430, 0)
|
||||
|
||||
@@ -30,7 +30,14 @@ func _init() -> void:
|
||||
# bcard 三竖卡(D1 232×352)+ 底部确认主钮与免费选择注记。差异白名单=免费选择/✓章/无价签。
|
||||
# 状态机不变:pick→result。M7 同构裁决遗产保留。
|
||||
var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, false)
|
||||
var top: HBoxContainer = page.top_row
|
||||
_build_top_row(page.top_row)
|
||||
var box: VBoxContainer = page.box
|
||||
_build_body(box)
|
||||
visible = false
|
||||
|
||||
|
||||
## 顶条域:标题+刷新图标钮(44×44 豁免宽度基准+角标叠加)+刷新余量行
|
||||
func _build_top_row(top: HBoxContainer) -> void:
|
||||
top.add_child(UiKit.label("战利品三选一",UiKit.FS_TITLE, true, UiKit.WARM))
|
||||
var top_sp := Control.new()
|
||||
top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -51,7 +58,10 @@ func _init() -> void:
|
||||
_refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.WARM_SOFT)
|
||||
_refresh_l.visible = false
|
||||
top.add_child(_refresh_l)
|
||||
var box: VBoxContainer = page.box
|
||||
|
||||
|
||||
## 主体域:入账信息行+三竖卡列+底部确认/继续钮与免费选择注记
|
||||
func _build_body(box: VBoxContainer) -> void:
|
||||
_label = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM_SOFT)
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_label)
|
||||
@@ -96,7 +106,7 @@ func open_pick(header: String, cards: Array) -> void:
|
||||
var idl := cid.to_lower()
|
||||
var band := UiKit.tg_band(idl) # 类型色带单点(TG_* 五族同源判定;aoe/blast 归 TG_AOE)
|
||||
# 类型脚注(T7 判官 checkpoint:demo .bcard .src 底部类型行——实机缺=卡下半空底;词=demo 同款)
|
||||
var card := UiKit.bcard(title, desc, band, _icon_for_id(idl), "", _type_label(idl))
|
||||
var card := UiKit.bcard(title, desc, band, _icon_for_id(idl), {"footnote": _type_label(idl)})
|
||||
card.gui_input.connect(func(ev: InputEvent) -> void:
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
_select_card(cid))
|
||||
@@ -189,19 +199,15 @@ func _emit_continue_pressed() -> void:
|
||||
|
||||
|
||||
# M7:id → 手绘图标键(子串映射容错 id 词表演进;缺位=BCard 空图标槽不炸)
|
||||
## 图标映射表(id 子串→图标键——判定序 heal/blast/gp/attack/armor/ap 缺省钱袋逐字)
|
||||
func _icon_for_id(idl: String) -> String:
|
||||
if idl.contains("heal"):
|
||||
return UiKit.TEX_ICON_MEDKIT
|
||||
if idl.contains("blast") or idl.contains("magic"):
|
||||
return UiKit.TEX_ICON_BLAST
|
||||
if idl.contains("gp") or idl.contains("coin"):
|
||||
return UiKit.TEX_ICON_COINBAG
|
||||
if idl.contains("attack"):
|
||||
return UiKit.TEX_ICON_SWORD
|
||||
if idl.contains("armor"):
|
||||
return UiKit.TEX_ICON_SHIELD
|
||||
if idl.contains("ap") or idl.contains("action"):
|
||||
return UiKit.TEX_ICON_POUCH
|
||||
var rules := [[["heal"], UiKit.TEX_ICON_MEDKIT], [["blast", "magic"], UiKit.TEX_ICON_BLAST],
|
||||
[["gp", "coin"], UiKit.TEX_ICON_COINBAG], [["attack"], UiKit.TEX_ICON_SWORD],
|
||||
[["armor"], UiKit.TEX_ICON_SHIELD], [["ap", "action"], UiKit.TEX_ICON_POUCH]]
|
||||
for rule in rules:
|
||||
for kw in (rule[0] as Array):
|
||||
if idl.contains(String(kw)):
|
||||
return String(rule[1])
|
||||
return UiKit.TEX_ICON_COINBAG
|
||||
|
||||
|
||||
|
||||
@@ -10,30 +10,36 @@ signal debug_levels_requested # 两层调试对局入口
|
||||
signal diagnose_requested # 诊断转储入口(卡死取证编排归组合根)
|
||||
signal closed
|
||||
|
||||
var seed_edit: LineEdit # 组合根 _seed_value 唯一读取面
|
||||
## 内部状态件(u7 子域拆件——契约 9 职责语句「调试浮窗运行态」):单一变化原因=调试面板状态与
|
||||
## 当局数据投影。探针直读冻结成员(seed_edit/_sim_btn/_dbg_btn)住宿主;
|
||||
## st._run_data/st._run_labels 经 run_data_probe()/update_run_data() 方法出口(字段直访面零外部依赖)。
|
||||
class _DebugState extends RefCounted:
|
||||
var _db
|
||||
var _base_atk := 0
|
||||
var _base_hp := 0
|
||||
var _atk: HSlider
|
||||
var _hp: HSlider
|
||||
var _atk_val: Label
|
||||
var _hp_val: Label
|
||||
var _eff: Label
|
||||
var _sim_count: SpinBox
|
||||
var _sim_out: RichTextLabel
|
||||
var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰)
|
||||
var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用)
|
||||
var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入)
|
||||
var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe)
|
||||
|
||||
var _db
|
||||
var _base_atk := 0
|
||||
var _base_hp := 0
|
||||
var _atk: HSlider
|
||||
var _hp: HSlider
|
||||
var _atk_val: Label
|
||||
var _hp_val: Label
|
||||
var _eff: Label
|
||||
var _sim_count: SpinBox
|
||||
var _sim_btn: Button
|
||||
var _dbg_btn: Button # 两层调试入口钮(对局中禁用承载面)
|
||||
var _sim_out: RichTextLabel
|
||||
var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰)
|
||||
var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用)
|
||||
var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入)
|
||||
var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe)
|
||||
|
||||
var st := _DebugState.new()
|
||||
var seed_edit: LineEdit # 组合根 _seed_value 唯一读取面(探针直读冻结)
|
||||
var _sim_btn: Button # 两层调试批量模拟入口钮(探针直读冻结)
|
||||
var _dbg_btn: Button # 两层调试入口钮(对局中禁用承载面;探针直读冻结)
|
||||
|
||||
|
||||
func _init(db_ref, base_atk: int, base_hp: int) -> void:
|
||||
_db = db_ref
|
||||
_base_atk = base_atk
|
||||
_base_hp = base_hp
|
||||
st._db = db_ref
|
||||
st._base_atk = base_atk
|
||||
st._base_hp = base_hp
|
||||
_build()
|
||||
_sync_labels()
|
||||
visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律)
|
||||
@@ -42,78 +48,78 @@ func _init(db_ref, base_atk: int, base_hp: int) -> void:
|
||||
# ---- 对外语义面(组合根/探针)----
|
||||
|
||||
func atk_pct() -> int:
|
||||
return int(_atk.value)
|
||||
return int(st._atk.value)
|
||||
|
||||
|
||||
func hp_pct() -> int:
|
||||
return int(_hp.value)
|
||||
return int(st._hp.value)
|
||||
|
||||
|
||||
func set_sliders(atk: int, hp: int) -> void:
|
||||
_atk.value = atk
|
||||
_hp.value = hp
|
||||
st._atk.value = atk
|
||||
st._hp.value = hp
|
||||
|
||||
|
||||
func reset_sliders() -> void:
|
||||
_atk.value = 0
|
||||
_hp.value = 0
|
||||
st._atk.value = 0
|
||||
st._hp.value = 0
|
||||
_sync_labels()
|
||||
|
||||
|
||||
func effect_text() -> String:
|
||||
return _eff.text
|
||||
return st._eff.text
|
||||
|
||||
|
||||
func set_sim_count(n: int) -> void:
|
||||
_sim_count.value = n
|
||||
st._sim_count.value = n
|
||||
|
||||
|
||||
func is_running() -> bool:
|
||||
return _sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑)
|
||||
return st._sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑)
|
||||
|
||||
|
||||
func set_sim_running(running: bool) -> void:
|
||||
_sim_running = running
|
||||
st._sim_running = running
|
||||
_sync_sim_button()
|
||||
|
||||
|
||||
# 对局中上下文锁定(组合根 _sync_debug_overlay 喂入):批量模拟与两层调试入口
|
||||
# 在活跃对局并存态不开放(长程异步模拟/并发对局编排与活跃结算并存态破坏状态机)
|
||||
func set_context_locked(locked: bool) -> void:
|
||||
_ctx_locked = locked
|
||||
st._ctx_locked = locked
|
||||
_sync_sim_button()
|
||||
|
||||
|
||||
# 当局数据只读投影喂入(六项——组合根 _debug_run_data 构造;纯标签重查零动效)
|
||||
func update_run_data(data: Dictionary) -> void:
|
||||
_run_data = data.duplicate()
|
||||
for key in _run_labels:
|
||||
(_run_labels[key] as Label).text = str(data.get(key, "—"))
|
||||
st._run_data = data.duplicate()
|
||||
for key in st._run_labels:
|
||||
(st._run_labels[key] as Label).text = str(data.get(key, "—"))
|
||||
|
||||
|
||||
# 当局数据快照读面(探针值对面——ui_tune_probe 断言六项与只读访问器逐项一致)
|
||||
func run_data_probe() -> Dictionary:
|
||||
return _run_data.duplicate()
|
||||
return st._run_data.duplicate()
|
||||
|
||||
|
||||
# 两钮同判据(模拟运行 / 对局上下文锁定对「模拟」「两层调试」两入口等价)——
|
||||
# 判据单点:两 setter 都经此同步,禁在调用点各写一份
|
||||
func _sync_sim_button() -> void:
|
||||
_sim_btn.disabled = _sim_running or _ctx_locked
|
||||
_sim_btn.text = "模拟中…" if _sim_running else "运行模拟"
|
||||
_dbg_btn.disabled = _sim_running or _ctx_locked
|
||||
_sim_btn.disabled = st._sim_running or st._ctx_locked
|
||||
_sim_btn.text = "模拟中…" if st._sim_running else "运行模拟"
|
||||
_dbg_btn.disabled = st._sim_running or st._ctx_locked
|
||||
|
||||
|
||||
func set_sim_progress(text: String) -> void:
|
||||
_sim_out.text = text
|
||||
st._sim_out.text = text
|
||||
|
||||
|
||||
func set_sim_report(text: String) -> void:
|
||||
_sim_out.text = text
|
||||
st._sim_out.text = text
|
||||
|
||||
|
||||
func output_text() -> String:
|
||||
return _sim_out.get_parsed_text()
|
||||
return st._sim_out.get_parsed_text()
|
||||
|
||||
|
||||
# ---- 探针接缝(真实翻译出口——按钮 pressed 信号路径,非直调组合根)----
|
||||
@@ -141,11 +147,11 @@ func _reset_btn_pressed() -> void:
|
||||
|
||||
|
||||
func effective_atk() -> int:
|
||||
return UiKit.scaled(_base_atk, atk_pct())
|
||||
return UiKit.scaled(st._base_atk, atk_pct())
|
||||
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
return UiKit.scaled(_base_hp, hp_pct())
|
||||
return UiKit.scaled(st._base_hp, hp_pct())
|
||||
|
||||
|
||||
# ---- 内部构建(标题/关闭固定于浮窗骨架,可扩展列表区居中滚动)----
|
||||
@@ -153,15 +159,7 @@ func effective_max_hp() -> int:
|
||||
func _build() -> void:
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(640, 0), 8)
|
||||
var box: VBoxContainer = shell.box
|
||||
|
||||
var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM)
|
||||
box.add_child(title)
|
||||
var desc := UiKit.label(
|
||||
"开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [_base_atk, _base_hp],
|
||||
13, false, UiKit.WARM_SOFT)
|
||||
box.add_child(desc)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
_build_intro(box)
|
||||
# 可扩展列表页(后续调试项按区块往下追加——超高滚动,滚轮下探)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(0, 430)
|
||||
@@ -171,10 +169,32 @@ func _build() -> void:
|
||||
list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
list.add_theme_constant_override("separation", 8)
|
||||
scroll.add_child(list)
|
||||
_build_run_data(list)
|
||||
_build_fx_row(list)
|
||||
_build_seed_row(list)
|
||||
_build_slider_block(list)
|
||||
_build_sim_block(list)
|
||||
_build_debug_diag(list)
|
||||
var close_btn := UiKit.button("关闭")
|
||||
close_btn.pressed.connect(_emit_closed)
|
||||
box.add_child(close_btn)
|
||||
|
||||
# 区块·当局数据(六项只读投影:种子=本局种子唯一查看位〔adventure
|
||||
# .master_seed 现值,非 _seed_value() 重算〕/关卡/节点/HP/GP/钥石;数据由组合根开窗与模态开合
|
||||
# 时 update_run_data 喂入;纯标签零信号连接零动效)
|
||||
|
||||
## 头部域:标题+说明(百分比叠加基础值——影响所有模式,不落盘)
|
||||
func _build_intro(box: VBoxContainer) -> void:
|
||||
var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM)
|
||||
box.add_child(title)
|
||||
var desc := UiKit.label(
|
||||
"开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [st._base_atk, st._base_hp],
|
||||
13, false, UiKit.WARM_SOFT)
|
||||
box.add_child(desc)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
|
||||
## 区块·当局数据(六项只读投影:种子=本局种子唯一查看位〔adventure
|
||||
## .master_seed 现值,非 _seed_value() 重算〕/关卡/节点/HP/GP/钥石;数据由组合根开窗与模态开合
|
||||
## 时 update_run_data 喂入;纯标签零信号连接零动效)
|
||||
func _build_run_data(list: VBoxContainer) -> void:
|
||||
var run_head := UiKit.label("当局数据(只读)",UiKit.FS_HINT, true, UiKit.WARM)
|
||||
list.add_child(run_head)
|
||||
var run_grid := GridContainer.new()
|
||||
@@ -187,11 +207,13 @@ func _build() -> void:
|
||||
cap.custom_minimum_size = Vector2(52, 0)
|
||||
run_grid.add_child(cap)
|
||||
var val := UiKit.label("—",UiKit.FS_HINT, false, UiKit.WARM)
|
||||
_run_labels[item[0]] = val
|
||||
st._run_labels[item[0]] = val
|
||||
run_grid.add_child(val)
|
||||
list.add_child(HSeparator.new())
|
||||
|
||||
# 区块⓪胜利演出风格(两套并存——纹章拼合[默认]/尘金粒子;会话级不落盘)
|
||||
|
||||
## 区块⓪胜利演出风格(两套并存——纹章拼合[默认]/尘金粒子;会话级不落盘)
|
||||
func _build_fx_row(list: VBoxContainer) -> void:
|
||||
var fx_row := HBoxContainer.new()
|
||||
fx_row.add_theme_constant_override("separation", 10)
|
||||
list.add_child(fx_row)
|
||||
@@ -201,8 +223,10 @@ func _build() -> void:
|
||||
fx_btn.pressed.connect(_on_fx_toggle.bind(fx_btn)) # lambda 转命名方法(只读巡检白名单方法名精确可判)
|
||||
fx_row.add_child(fx_btn)
|
||||
|
||||
# 区块①随机种子(种子三分判定归组合根 _seed_value 单点):
|
||||
# 默认空框(空=随全局规则:首局恒 42/非首局随机;显式填入全场景优先)
|
||||
|
||||
## 区块①随机种子(种子三分判定归组合根 _seed_value 单点):
|
||||
## 默认空框(空=随全局规则:首局恒 42/非首局随机;显式填入全场景优先)
|
||||
func _build_seed_row(list: VBoxContainer) -> void:
|
||||
var seed_row := HBoxContainer.new()
|
||||
list.add_child(seed_row)
|
||||
seed_row.add_child(UiKit.label("随机种子",UiKit.FS_BODY, false))
|
||||
@@ -213,16 +237,17 @@ func _build() -> void:
|
||||
seed_row.add_child(seed_edit)
|
||||
list.add_child(HSeparator.new())
|
||||
|
||||
# 区块②角色数值调整板
|
||||
var atk_row := _slider_row(list, "攻击加成")
|
||||
_atk = atk_row.slider
|
||||
_atk_val = atk_row.value_label
|
||||
var hp_row := _slider_row(list, "生命加成")
|
||||
_hp = hp_row.slider
|
||||
_hp_val = hp_row.value_label
|
||||
_eff = UiKit.label("",UiKit.FS_HINT, false)
|
||||
list.add_child(_eff)
|
||||
|
||||
## 区块②角色数值调整板(攻击/生命两滑轨+生效值行+应用/重置钮)
|
||||
func _build_slider_block(list: VBoxContainer) -> void:
|
||||
var atk_row := _slider_row(list, "攻击加成")
|
||||
st._atk = atk_row.slider
|
||||
st._atk_val = atk_row.value_label
|
||||
var hp_row := _slider_row(list, "生命加成")
|
||||
st._hp = hp_row.slider
|
||||
st._hp_val = hp_row.value_label
|
||||
st._eff = UiKit.label("",UiKit.FS_HINT, false)
|
||||
list.add_child(st._eff)
|
||||
var btn_row := HBoxContainer.new()
|
||||
list.add_child(btn_row)
|
||||
var apply_btn := UiKit.button("应用并关闭")
|
||||
@@ -233,34 +258,36 @@ func _build() -> void:
|
||||
btn_row.add_child(reset_btn)
|
||||
list.add_child(HSeparator.new())
|
||||
|
||||
# 区块③批量模拟(与 headless 扫描同口径——编排归 tune_sim_controller)
|
||||
|
||||
## 区块③批量模拟(与 headless 扫描同口径——编排归 tune_sim_controller)
|
||||
func _build_sim_block(list: VBoxContainer) -> void:
|
||||
var sim_row := HBoxContainer.new()
|
||||
list.add_child(sim_row)
|
||||
sim_row.add_child(UiKit.label("批量模拟第一关",UiKit.FS_BODY, false))
|
||||
_sim_count = SpinBox.new()
|
||||
_sim_count.min_value = 10
|
||||
_sim_count.max_value = 200
|
||||
_sim_count.step = 10
|
||||
_sim_count.value = 30
|
||||
_sim_count.custom_minimum_size = Vector2(90, 0)
|
||||
sim_row.add_child(_sim_count)
|
||||
st._sim_count = SpinBox.new()
|
||||
st._sim_count.min_value = 10
|
||||
st._sim_count.max_value = 200
|
||||
st._sim_count.step = 10
|
||||
st._sim_count.value = 30
|
||||
st._sim_count.custom_minimum_size = Vector2(90, 0)
|
||||
sim_row.add_child(st._sim_count)
|
||||
sim_row.add_child(UiKit.label("次(理性玩家策略,含面板当前加成)",UiKit.FS_HINT, false))
|
||||
_sim_btn = UiKit.button("运行模拟")
|
||||
_sim_btn.pressed.connect(_emit_simulate_requested)
|
||||
sim_row.add_child(_sim_btn)
|
||||
_sim_out = RichTextLabel.new()
|
||||
_sim_out.bbcode_enabled = true
|
||||
_sim_out.custom_minimum_size = Vector2(0, 300)
|
||||
list.add_child(_sim_out)
|
||||
st._sim_out = RichTextLabel.new()
|
||||
st._sim_out.bbcode_enabled = true
|
||||
st._sim_out.custom_minimum_size = Vector2(0, 300)
|
||||
list.add_child(st._sim_out)
|
||||
list.add_child(HSeparator.new())
|
||||
|
||||
# 区块④两层调试对局入口(HP 关内延续;对局中禁用)
|
||||
|
||||
## 区块④⑤两层调试入口与诊断转储(对局中禁用;转储编排经信号外发归组合根——纯视图边界;
|
||||
## dump_diagnosis 卡死取证功能保留,不设门控隐藏——本浮窗本就可全局可开,双轨无意义)
|
||||
func _build_debug_diag(list: VBoxContainer) -> void:
|
||||
_dbg_btn = UiKit.button("调试 · 连续关卡两层(HP 关内延续)")
|
||||
_dbg_btn.pressed.connect(_emit_debug_levels)
|
||||
list.add_child(_dbg_btn)
|
||||
|
||||
# 区块⑤诊断转储:dump_diagnosis 卡死取证功能保留;不设门控隐藏——本浮窗本就可全局可开,双轨无意义。
|
||||
# 纯视图边界:转储编排经信号外发归组合根。
|
||||
var diag_row := HBoxContainer.new()
|
||||
diag_row.add_theme_constant_override("separation", 10)
|
||||
list.add_child(diag_row)
|
||||
@@ -270,10 +297,6 @@ func _build() -> void:
|
||||
diag_btn.pressed.connect(_emit_diagnose_requested)
|
||||
diag_row.add_child(diag_btn)
|
||||
|
||||
var close_btn := UiKit.button("关闭")
|
||||
close_btn.pressed.connect(_emit_closed)
|
||||
box.add_child(close_btn)
|
||||
|
||||
|
||||
func _slider_row(box: VBoxContainer, title: String) -> Dictionary:
|
||||
var row := HBoxContainer.new()
|
||||
@@ -293,14 +316,14 @@ func _slider_row(box: VBoxContainer, title: String) -> Dictionary:
|
||||
|
||||
|
||||
func _sync_labels() -> void:
|
||||
_atk_val.text = "+%d%%" % atk_pct()
|
||||
_hp_val.text = "+%d%%" % hp_pct()
|
||||
_eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [
|
||||
_base_atk, effective_atk(), _base_hp, effective_max_hp()]
|
||||
st._atk_val.text = "+%d%%" % atk_pct()
|
||||
st._hp_val.text = "+%d%%" % hp_pct()
|
||||
st._eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [
|
||||
st._base_atk, effective_atk(), st._base_hp, effective_max_hp()]
|
||||
|
||||
|
||||
func _emit_simulate_requested() -> void:
|
||||
simulate_requested.emit(int(_sim_count.value))
|
||||
simulate_requested.emit(int(st._sim_count.value))
|
||||
|
||||
|
||||
func _emit_diagnose_requested() -> void:
|
||||
|
||||
@@ -30,9 +30,14 @@ func _init() -> void:
|
||||
var col := VBoxContainer.new()
|
||||
col.add_theme_constant_override("separation", 14) # 布局充实(区段间距放大)
|
||||
add_child(col)
|
||||
_build_data_row(col)
|
||||
_build_slots(col)
|
||||
_build_side_sections(col)
|
||||
|
||||
|
||||
## 数据行域(直角细线数据框——battle.md Component Inventory C 区行规格落形;同时消化纵向空白)
|
||||
func _build_data_row(col: VBoxContainer) -> void:
|
||||
col.add_child(UiKit.label("探险者",UiKit.FS_SUB, true))
|
||||
# 数据行装框(直角细线数据框——battle.md Component Inventory C 区行规格落形;同时消化纵向空白)
|
||||
var data_box := PanelContainer.new()
|
||||
data_box.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(6, 4, 6, 4)))
|
||||
@@ -45,55 +50,60 @@ func _init() -> void:
|
||||
_armor_label = _data_line()
|
||||
data_col.add_child(_armor_label)
|
||||
|
||||
# 秘宝槽(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此,
|
||||
# 不预设交互(挖掘期指向交互归 E 区既有裁决)
|
||||
|
||||
## 秘宝槽域(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此,
|
||||
## 不预设交互(挖掘期指向交互归 E 区既有裁决)
|
||||
func _build_slots(col: VBoxContainer) -> void:
|
||||
col.add_child(UiKit.label("秘宝",UiKit.FS_BODY, true))
|
||||
item_slots = HBoxContainer.new()
|
||||
item_slots.add_theme_constant_override("separation", 10)
|
||||
col.add_child(item_slots)
|
||||
for i in 4:
|
||||
var slot := PanelContainer.new()
|
||||
slot.custom_minimum_size = Vector2(48, 48)
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4)))
|
||||
var idx := i
|
||||
slot.gui_input.connect(func(ev: InputEvent):
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
# 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态)
|
||||
treasure_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0))
|
||||
item_slots.add_child(slot)
|
||||
_slot_panels.append(slot)
|
||||
_slot_panels.append(_build_slot(i))
|
||||
|
||||
# 特性持有段(三选一特性兑付后常驻可读:逐件行「件名 ×N」;空态整节隐藏。
|
||||
# 上移承载——构筑核心信息优先级高于临时强化,且防 C 区下部裁切不可见)
|
||||
_feat_section = VBoxContainer.new()
|
||||
_feat_section.add_theme_constant_override("separation", 4)
|
||||
_feat_section.visible = false
|
||||
col.add_child(_feat_section)
|
||||
_feat_section.add_child(UiKit.label("专长",UiKit.FS_BODY, true))
|
||||
_feat_list = VBoxContainer.new()
|
||||
_feat_list.add_theme_constant_override("separation", 2)
|
||||
_feat_section.add_child(_feat_list)
|
||||
# 地形变异常驻角标(Boss 图整图型战斗全程可见防中局遗忘;
|
||||
# 空态整节隐藏=特性段先例;摆位/文案 provisional)
|
||||
_board_variant_section = VBoxContainer.new()
|
||||
_board_variant_section.add_theme_constant_override("separation", 4)
|
||||
_board_variant_section.visible = false
|
||||
col.add_child(_board_variant_section)
|
||||
_board_variant_section.add_child(UiKit.label("地形变异",UiKit.FS_BODY, true))
|
||||
_board_variant_list = VBoxContainer.new()
|
||||
_board_variant_list.add_theme_constant_override("separation", 2)
|
||||
_board_variant_section.add_child(_board_variant_list)
|
||||
# 特性(预留):锻造/大奖励/关卡奖励节点的属性提升——中循环数据因改变战斗数学准入;
|
||||
# 与战斗内状态效果(B 区站桩,随战斗消散)分区,防「战斗结束被清」误读;空态整节隐藏
|
||||
_curio_section = VBoxContainer.new()
|
||||
_curio_section.add_theme_constant_override("separation", 4)
|
||||
_curio_section.visible = false
|
||||
col.add_child(_curio_section)
|
||||
_curio_section.add_child(UiKit.label("属性值提升",UiKit.FS_BODY, true))
|
||||
_curio_list = VBoxContainer.new()
|
||||
_curio_list.add_theme_constant_override("separation", 2)
|
||||
_curio_section.add_child(_curio_list)
|
||||
|
||||
## 单槽构建域:48px 槽牌+点击路由(空槽/越界槽点击=取消选中 handle 0;有效槽发其 handle)
|
||||
func _build_slot(i: int) -> PanelContainer:
|
||||
var slot := PanelContainer.new()
|
||||
slot.custom_minimum_size = Vector2(48, 48)
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4)))
|
||||
var idx := i
|
||||
slot.gui_input.connect(func(ev: InputEvent):
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
# 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态)
|
||||
treasure_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0))
|
||||
item_slots.add_child(slot)
|
||||
return slot
|
||||
|
||||
|
||||
## 三区段域(特性持有段上移承载——构筑核心信息优先级高于临时强化,且防 C 区下部裁切不可见/
|
||||
## 地形变异常驻角标 Boss 图整图型战斗全程可见防中局遗忘/特性预留段防「战斗结束被清」误读;空态整节隐藏)
|
||||
func _build_side_sections(col: VBoxContainer) -> void:
|
||||
_feat_section = _labeled_section(col, "专长")
|
||||
_feat_list = _list_of(_feat_section)
|
||||
_board_variant_section = _labeled_section(col, "地形变异")
|
||||
_board_variant_list = _list_of(_board_variant_section)
|
||||
_curio_section = _labeled_section(col, "属性值提升")
|
||||
_curio_list = _list_of(_curio_section)
|
||||
|
||||
|
||||
## 区段骨架(标题+隐藏容器)
|
||||
func _labeled_section(col: VBoxContainer, title: String) -> VBoxContainer:
|
||||
var section := VBoxContainer.new()
|
||||
section.add_theme_constant_override("separation", 4)
|
||||
section.visible = false
|
||||
col.add_child(section)
|
||||
section.add_child(UiKit.label(title,UiKit.FS_BODY, true))
|
||||
return section
|
||||
|
||||
|
||||
## 行板容器(separation 2)
|
||||
func _list_of(section: VBoxContainer) -> VBoxContainer:
|
||||
var list := VBoxContainer.new()
|
||||
list.add_theme_constant_override("separation", 2)
|
||||
section.add_child(list)
|
||||
return list
|
||||
|
||||
|
||||
func set_attack(atk: int, mod_text: String) -> void:
|
||||
@@ -174,46 +184,69 @@ func set_treasure_inventory(items: Array, selected_handle: int) -> void:
|
||||
continue
|
||||
var rec: Dictionary[Variant, Variant] = items[i]
|
||||
_slot_handles.append(int(rec.get("handle", 0)))
|
||||
var used := bool(rec.get("used", false))
|
||||
var usable := bool(rec.get("usable", true))
|
||||
var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle
|
||||
# 槽内容=手绘像素图标(demo ⑦;glyph 字符→图标键映射,未映射/used/不可用回退字形+条件墨色)
|
||||
var glyph_ch := String(rec.get("glyph", "?"))
|
||||
var icon_map := {"爆": UiKit.TEX_ICON_BLAST, "医": UiKit.TEX_ICON_MEDKIT,
|
||||
"铲": UiKit.TEX_ICON_SHOVEL, "袋": UiKit.TEX_ICON_COINBAG, "钥": UiKit.TEX_ICON_KEYSHARD,
|
||||
"剑": UiKit.TEX_ICON_SWORD, "盾": UiKit.TEX_ICON_SHIELD, "心": UiKit.TEX_ICON_HEART,
|
||||
"行": UiKit.TEX_ICON_POUCH}
|
||||
var icon_path: String = icon_map.get(glyph_ch, "")
|
||||
var itex := UiKit.load_tex(icon_path) if icon_path != "" else null
|
||||
if itex != null and not used and usable:
|
||||
var tr := TextureRect.new()
|
||||
tr.texture = itex
|
||||
tr.custom_minimum_size = Vector2(40, 40)
|
||||
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
slot.add_child(tr)
|
||||
else:
|
||||
var glyph := UiKit.label(glyph_ch,UiKit.FS_SUB, true)
|
||||
glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
glyph.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if used:
|
||||
# 「已用」占位墨=灰石深化(STONE_FAINT 对 PANEL_DATA 槽牌 2.7:1;
|
||||
# STONE_DEEP = 5.6:1;语义分档保持双墨)
|
||||
glyph.add_theme_color_override("font_color", UiKit.STONE_DEEP)
|
||||
elif not usable:
|
||||
# 「不可用」置灰墨=标签深墨(STONE 对槽牌 3.4:1;INK_LABEL = 10.4:1,虚位/失效更强置灰语义)
|
||||
glyph.add_theme_color_override("font_color", UiKit.INK_LABEL)
|
||||
slot.add_child(glyph)
|
||||
var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4)))
|
||||
# note=积点进度行(自动兑付件悬停浮窗补「积点 N/M」,进度随挖格经组合根重查刷新)
|
||||
var note := String(rec.get("note", ""))
|
||||
slot.tooltip_text = "%s%s%s" % [String(rec.get("name", "")),
|
||||
"(%s)" % note if note != "" else "",
|
||||
"(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")]
|
||||
_fill_slot(slot, rec, selected_handle)
|
||||
|
||||
|
||||
## 单槽内容填充域:图标优先(像素图标未映射/used/不可用回退字形+条件墨色)+选中金框+悬停浮窗
|
||||
func _fill_slot(slot: PanelContainer, rec: Dictionary, selected_handle: int) -> void:
|
||||
var used := bool(rec.get("used", false))
|
||||
var usable := bool(rec.get("usable", true))
|
||||
var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle
|
||||
# 槽内容=手绘像素图标(demo ⑦;glyph 字符→图标键映射,未映射/used/不可用回退字形+条件墨色)
|
||||
var itex := _slot_icon_tex(String(rec.get("glyph", "?")))
|
||||
if itex != null and not used and usable:
|
||||
slot.add_child(_slot_icon_rect(itex))
|
||||
else:
|
||||
slot.add_child(_slot_glyph_label(String(rec.get("glyph", "?")), used, usable))
|
||||
var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4)))
|
||||
slot.tooltip_text = _slot_tooltip(rec, used, usable)
|
||||
|
||||
|
||||
## 字形→图标纹理映射(demo ⑦ 键表单点)
|
||||
func _slot_icon_tex(glyph_ch: String) -> Texture2D:
|
||||
var icon_map := {"爆": UiKit.TEX_ICON_BLAST, "医": UiKit.TEX_ICON_MEDKIT,
|
||||
"铲": UiKit.TEX_ICON_SHOVEL, "袋": UiKit.TEX_ICON_COINBAG, "钥": UiKit.TEX_ICON_KEYSHARD,
|
||||
"剑": UiKit.TEX_ICON_SWORD, "盾": UiKit.TEX_ICON_SHIELD, "心": UiKit.TEX_ICON_HEART,
|
||||
"行": UiKit.TEX_ICON_POUCH}
|
||||
var icon_path: String = icon_map.get(glyph_ch, "")
|
||||
return UiKit.load_tex(icon_path) if icon_path != "" else null
|
||||
|
||||
|
||||
## 图标矩形域(40px 素描框,像素过滤)
|
||||
func _slot_icon_rect(itex: Texture2D) -> TextureRect:
|
||||
var tr := TextureRect.new()
|
||||
tr.texture = itex
|
||||
tr.custom_minimum_size = Vector2(40, 40)
|
||||
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return tr
|
||||
|
||||
|
||||
## 字形回退标签域(used=灰石深化墨 5.6:1/不可用=标签深墨 10.4:1 虚位失效更强置灰——语义分档双墨)
|
||||
func _slot_glyph_label(glyph_ch: String, used: bool, usable: bool) -> Label:
|
||||
var glyph := UiKit.label(glyph_ch,UiKit.FS_SUB, true)
|
||||
glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
glyph.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if used:
|
||||
# 「已用」占位墨=灰石深化(STONE_FAINT 对 PANEL_DATA 槽牌 2.7:1;
|
||||
# STONE_DEEP = 5.6:1;语义分档保持双墨)
|
||||
glyph.add_theme_color_override("font_color", UiKit.STONE_DEEP)
|
||||
elif not usable:
|
||||
# 「不可用」置灰墨=标签深墨(STONE 对槽牌 3.4:1;INK_LABEL = 10.4:1,虚位/失效更强置灰语义)
|
||||
glyph.add_theme_color_override("font_color", UiKit.INK_LABEL)
|
||||
return glyph
|
||||
|
||||
|
||||
## 悬停浮窗域(note=积点进度行——自动兑付件悬停浮窗补「积点 N/M」,进度随挖格经组合根重查刷新)
|
||||
func _slot_tooltip(rec: Dictionary, used: bool, usable: bool) -> String:
|
||||
var note := String(rec.get("note", ""))
|
||||
return "%s%s%s" % [String(rec.get("name", "")),
|
||||
"(%s)" % note if note != "" else "",
|
||||
"(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")]
|
||||
@@ -23,7 +23,7 @@ var _rap_cost := -1 # <0 = 不渲染 RAP 行(构造参数注表值,禁字
|
||||
|
||||
func _init(rap_cost: int = -1) -> void:
|
||||
_rap_cost = rap_cost
|
||||
var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6, true,
|
||||
var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6,
|
||||
UiKit.panel_skinned(Vector4(12, 10, 12, 10))) # 石板皮(T2;缺位回退 flat)
|
||||
_panel = shell.panel
|
||||
(shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input)
|
||||
@@ -32,17 +32,7 @@ func _init(rap_cost: int = -1) -> void:
|
||||
box.add_child(title)
|
||||
box.add_child(HSeparator.new())
|
||||
for e in ENTRIES:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
var chip := ColorRect.new()
|
||||
chip.custom_minimum_size = Vector2(18, 18)
|
||||
chip.color = Color(e[0])
|
||||
row.add_child(chip)
|
||||
var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面)
|
||||
lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
lab.custom_minimum_size = Vector2(440, 0)
|
||||
row.add_child(lab)
|
||||
box.add_child(row)
|
||||
box.add_child(_legend_row(e))
|
||||
box.add_child(HSeparator.new())
|
||||
# RAP 行(表值驱动,禁字面量:刷新代价读行动经济表)
|
||||
if _rap_cost >= 0:
|
||||
@@ -64,6 +54,24 @@ func _init(rap_cost: int = -1) -> void:
|
||||
visible = false
|
||||
|
||||
|
||||
|
||||
## 单行域(循环体提函数——色块 chip+词条 18px 网格对齐)
|
||||
func _legend_row(e: Array) -> HBoxContainer:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
var chip := ColorRect.new()
|
||||
chip.custom_minimum_size = Vector2(18, 18)
|
||||
chip.color = Color(e[0])
|
||||
row.add_child(chip)
|
||||
var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面)
|
||||
lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
lab.custom_minimum_size = Vector2(440, 0)
|
||||
row.add_child(lab)
|
||||
return row
|
||||
|
||||
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
@@ -31,43 +31,47 @@ const HIT_PAD := 6.0 # 图上命中外扩(指
|
||||
const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落)
|
||||
const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit)
|
||||
|
||||
var _runner = null
|
||||
var _title: Label
|
||||
var _canvas: Control
|
||||
var _level_label: Label
|
||||
var _gp_value: Label
|
||||
var _hp_value: Label
|
||||
var _hp_bar: Control
|
||||
var _attr_value: Label
|
||||
var _bonus_row: HBoxContainer
|
||||
var _bonus_value: Label
|
||||
var _feat_value: Label # 特性持有行(组合根投喂逐件文案)
|
||||
var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集)
|
||||
var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它)
|
||||
var _hover_id := -1 # 悬停预承诺候选(-1 = 无)
|
||||
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开)
|
||||
var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根)
|
||||
var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S)
|
||||
var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面)
|
||||
var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全)
|
||||
var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点)
|
||||
var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用)
|
||||
var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效)
|
||||
## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因=
|
||||
## 选路呈现)。_layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。
|
||||
## 探针直读冻结成员(_cand_ids/_hover_id/_content_size/_layout_cache/abandon_btn/status_badge)住宿主。
|
||||
class _MapState extends RefCounted:
|
||||
var _runner = null
|
||||
var _title: Label
|
||||
var _canvas: Control
|
||||
var _level_label: Label
|
||||
var _gp_value: Label
|
||||
var _hp_value: Label
|
||||
var _hp_bar: Control
|
||||
var _attr_value: Label
|
||||
var _bonus_row: HBoxContainer
|
||||
var _bonus_value: Label
|
||||
var _feat_value: Label # 特性持有行(组合根投喂逐件文案)
|
||||
var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S)
|
||||
var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面)
|
||||
var _layer_decor := {"node_icons": {}, "noise_rect": null, "tint_level": 0} # 换层装饰域(节点图标贴图 load_tex 守卫缺位 null→字形回退/屏底石纹面/当前已应用 tint 层号 0=未应用)
|
||||
|
||||
var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主)
|
||||
var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结)
|
||||
var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结)
|
||||
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结)
|
||||
var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结)
|
||||
var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结)
|
||||
var _map := _MapState.new()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_abandon_timer.expired.connect(_reset_abandon_confirm)
|
||||
_map._abandon_timer.expired.connect(_reset_abandon_confirm)
|
||||
# 节点图标贴图装载(load_tex 守卫,缺位回退现行字形)
|
||||
for t in TEX_NODE_ICONS:
|
||||
_node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t])
|
||||
_map._layer_decor.node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t])
|
||||
# 独立屏底(spec Layout——demo #v-map 暖棕竖向
|
||||
# 渐变(#4A3B2A 0%→#3A2E22 40%)+石纹弱质感 0.14+暗角聚焦;底纹缺位回退纯色)
|
||||
var base := UiKit.gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT)
|
||||
var base := UiKit._gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT)
|
||||
add_child(base)
|
||||
var tex := UiKit.backdrop_tile()
|
||||
if tex != null:
|
||||
base.add_child(tex)
|
||||
_noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档)
|
||||
_map._layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档)
|
||||
base.add_child(UiKit.vignette_rect())
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -75,9 +79,16 @@ func _init() -> void:
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
center.add_child(box)
|
||||
# 顶栏语境(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框;
|
||||
# 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+
|
||||
# 左右两极排布)
|
||||
_build_top_bar(box)
|
||||
_build_canvas(box)
|
||||
_build_status_bar(box)
|
||||
visible = false
|
||||
|
||||
|
||||
## 顶栏域(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框;
|
||||
## 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ 左右两极排布;
|
||||
## 状态角标置灰守卫归组合根 _sync_status_entry,信号连接归组合根——视图零连接)
|
||||
func _build_top_bar(box: VBoxContainer) -> void:
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 12)
|
||||
top.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -86,8 +97,8 @@ func _init() -> void:
|
||||
lvl_plate.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1, Vector4(12, 3, 12, 3)))
|
||||
top.add_child(lvl_plate)
|
||||
_level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM)
|
||||
lvl_plate.add_child(_level_label)
|
||||
_map._level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM)
|
||||
lvl_plate.add_child(_map._level_label)
|
||||
var top_sp := Control.new()
|
||||
top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
top.add_child(top_sp)
|
||||
@@ -98,31 +109,34 @@ func _init() -> void:
|
||||
var gp_row := HBoxContainer.new()
|
||||
gp_row.add_theme_constant_override("separation", 6)
|
||||
gp_box.add_child(gp_row)
|
||||
_gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD)
|
||||
gp_row.add_child(_gp_value)
|
||||
# 状态面板入口角标(顶栏常驻;本屏只留状态——调试角标归全局浮层。
|
||||
# 置灰守卫归组合根 _sync_status_entry(其他模态开着=不可点;本屏自身的登记开态不计——has_open_excluding);
|
||||
# 信号连接归组合根(视图零连接))
|
||||
_map._gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD)
|
||||
gp_row.add_child(_map._gp_value)
|
||||
status_badge = UiKit.button("状态", 0.0)
|
||||
status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT)
|
||||
status_badge.tooltip_text = "查看本局冒险进度(只读)"
|
||||
status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
top.add_child(status_badge)
|
||||
_title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM)
|
||||
_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_title)
|
||||
# 节点图画布(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击)
|
||||
_canvas = Control.new()
|
||||
_canvas.custom_minimum_size = Vector2(620, 240)
|
||||
_canvas.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_canvas.draw.connect(_draw_graph.bind(_canvas))
|
||||
_canvas.gui_input.connect(_on_canvas_input)
|
||||
box.add_child(_canvas)
|
||||
# 底部状态栏(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④;
|
||||
# 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权);
|
||||
# _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留)
|
||||
# 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆:
|
||||
# 特性持有行限宽截断+tooltip 全文)
|
||||
_map._title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM)
|
||||
_map._title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_map._title)
|
||||
|
||||
|
||||
## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击)
|
||||
func _build_canvas(box: VBoxContainer) -> void:
|
||||
_map._canvas = Control.new()
|
||||
_map._canvas.custom_minimum_size = Vector2(620, 240)
|
||||
_map._canvas.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_map._canvas.draw.connect(_draw_graph.bind(_map._canvas))
|
||||
_map._canvas.gui_input.connect(_on_canvas_input)
|
||||
box.add_child(_map._canvas)
|
||||
|
||||
|
||||
## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④;
|
||||
## 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权);
|
||||
## _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留。
|
||||
## 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆:
|
||||
## 特性持有行限宽截断+tooltip 全文))
|
||||
func _build_status_bar(box: VBoxContainer) -> void:
|
||||
var bottom := PanelContainer.new()
|
||||
bottom.custom_minimum_size = Vector2(0, 85)
|
||||
bottom.size_flags_vertical = Control.SIZE_SHRINK_END
|
||||
@@ -132,9 +146,37 @@ func _init() -> void:
|
||||
var strip := HBoxContainer.new()
|
||||
strip.add_theme_constant_override("separation", 18)
|
||||
bottom.add_child(strip)
|
||||
strip.add_child(_build_face_rect())
|
||||
var life_box := VBoxContainer.new()
|
||||
strip.add_child(life_box)
|
||||
life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT))
|
||||
_map._hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE)
|
||||
life_box.add_child(_map._hp_value)
|
||||
_map._hp_bar = Control.new()
|
||||
_map._hp_bar.custom_minimum_size = Vector2(130, 7)
|
||||
_map._hp_bar.draw.connect(_draw_hp_bar.bind(_map._hp_bar))
|
||||
life_box.add_child(_map._hp_bar)
|
||||
strip.add_child(_vdiv())
|
||||
var attr_box := VBoxContainer.new()
|
||||
strip.add_child(attr_box)
|
||||
attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT))
|
||||
_map._attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE)
|
||||
attr_box.add_child(_map._attr_value)
|
||||
strip.add_child(_vdiv())
|
||||
_build_bonus_group(strip)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
strip.add_child(spacer)
|
||||
abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38
|
||||
abandon_btn.pressed.connect(_on_abandon_pressed)
|
||||
abandon_btn.visible = false
|
||||
strip.add_child(abandon_btn)
|
||||
|
||||
|
||||
## 头像域:定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除)
|
||||
func _build_face_rect() -> TextureRect:
|
||||
var face := TextureRect.new()
|
||||
var face_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT)
|
||||
# 定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除)
|
||||
var face_h := 48.0
|
||||
var face_w := face_h
|
||||
if face_tex != null:
|
||||
@@ -145,50 +187,26 @@ func _init() -> void:
|
||||
face.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
face.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
face.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
strip.add_child(face)
|
||||
var life_box := VBoxContainer.new()
|
||||
strip.add_child(life_box)
|
||||
life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT))
|
||||
_hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE)
|
||||
life_box.add_child(_hp_value)
|
||||
_hp_bar = Control.new()
|
||||
_hp_bar.custom_minimum_size = Vector2(130, 7)
|
||||
_hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar))
|
||||
life_box.add_child(_hp_bar)
|
||||
strip.add_child(_vdiv())
|
||||
var attr_box := VBoxContainer.new()
|
||||
strip.add_child(attr_box)
|
||||
attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT))
|
||||
_attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE)
|
||||
attr_box.add_child(_attr_value)
|
||||
strip.add_child(_vdiv())
|
||||
# 本关加成框(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂)
|
||||
_bonus_row = HBoxContainer.new()
|
||||
strip.add_child(_bonus_row)
|
||||
return face
|
||||
|
||||
|
||||
## 本关加成框域(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂;限宽单行截断+tooltip 全文)
|
||||
func _build_bonus_group(strip: HBoxContainer) -> void:
|
||||
_map._bonus_row = HBoxContainer.new()
|
||||
strip.add_child(_map._bonus_row)
|
||||
var bonus_box := VBoxContainer.new()
|
||||
_bonus_row.add_child(bonus_box)
|
||||
_map._bonus_row.add_child(bonus_box)
|
||||
var bonus_inner := VBoxContainer.new()
|
||||
bonus_box.add_child(bonus_inner)
|
||||
var bonus_cap := UiKit.label("属性值提升",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读)
|
||||
bonus_inner.add_child(bonus_cap)
|
||||
_bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上)
|
||||
bonus_inner.add_child(_bonus_value)
|
||||
# 特性持有行(组合根投喂)
|
||||
# 限宽单行截断+tooltip 全文;
|
||||
# 墨色 INK_HINT(暖白系对亮板 ≈1:1 不可读——§7.2)
|
||||
_feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT)
|
||||
_feat_value.custom_minimum_size = Vector2(240, 0)
|
||||
_feat_value.clip_text = true
|
||||
_feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
bonus_box.add_child(_feat_value)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
strip.add_child(spacer)
|
||||
abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38
|
||||
abandon_btn.pressed.connect(_on_abandon_pressed)
|
||||
abandon_btn.visible = false
|
||||
strip.add_child(abandon_btn)
|
||||
visible = false
|
||||
_map._bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上)
|
||||
bonus_inner.add_child(_map._bonus_value)
|
||||
_map._feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT)
|
||||
_map._feat_value.custom_minimum_size = Vector2(240, 0)
|
||||
_map._feat_value.clip_text = true
|
||||
_map._feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
bonus_box.add_child(_map._feat_value)
|
||||
|
||||
|
||||
# 竖分隔线(底栏分组线——亮面板上低对比细线)
|
||||
@@ -202,10 +220,10 @@ static func _vdiv() -> ColorRect:
|
||||
func _draw_hp_bar(bar: Control) -> void:
|
||||
# 迷你血条(深底 UiKit.HP_BAR_BACK #28221C + 小人蓝填充——与战斗站桩血条同语言,
|
||||
# 「蓝只说己方」§4.2)。比例 = hero_hp / effective_max_hp
|
||||
if _runner == null:
|
||||
if _map._runner == null:
|
||||
return
|
||||
var max_hp := maxi(1, _runner.effective_max_hp())
|
||||
var ratio := clampf(float(_runner.hero_hp) / float(max_hp), 0.0, 1.0)
|
||||
var max_hp := maxi(1, _map._runner.effective_max_hp())
|
||||
var ratio := clampf(float(_map._runner.hero_hp) / float(max_hp), 0.0, 1.0)
|
||||
var full := Rect2(Vector2.ZERO, bar.size)
|
||||
bar.draw_rect(full, COL_HP_BAR_BACK)
|
||||
if ratio > 0.0:
|
||||
@@ -215,8 +233,8 @@ func _draw_hp_bar(bar: Control) -> void:
|
||||
# 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险)
|
||||
# 特性持有投影(组合根投喂逐件文案;空文案=行隐藏)
|
||||
func set_feat_holdings(text: String) -> void:
|
||||
_feat_value.text = text
|
||||
_feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文)
|
||||
_map._feat_value.text = text
|
||||
_map._feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文)
|
||||
|
||||
|
||||
func set_abandon_available(v: bool) -> void:
|
||||
@@ -226,8 +244,8 @@ func set_abandon_available(v: bool) -> void:
|
||||
# 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。
|
||||
# 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。
|
||||
func _on_abandon_pressed() -> void:
|
||||
if not _abandon_timer.is_armed():
|
||||
_abandon_timer.arm(CONFIRM_WINDOW_S)
|
||||
if not _map._abandon_timer.is_armed():
|
||||
_map._abandon_timer.arm(CONFIRM_WINDOW_S)
|
||||
abandon_btn.text = "再点一次确认放弃——不可逆"
|
||||
if ArmedTimer.motion_still():
|
||||
abandon_btn.modulate = Color(1, 1, 1, 0.75) # 减动效静态基色(脉冲层不再驱动)
|
||||
@@ -237,45 +255,45 @@ func _on_abandon_pressed() -> void:
|
||||
|
||||
|
||||
func _reset_abandon_confirm() -> void:
|
||||
_abandon_timer.disarm()
|
||||
_map._abandon_timer.disarm()
|
||||
abandon_btn.modulate = Color(1, 1, 1, 1)
|
||||
abandon_btn.text = "放弃" # 文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _abandon_timer.is_armed():
|
||||
if not _map._abandon_timer.is_armed():
|
||||
return
|
||||
_abandon_timer.tick(delta)
|
||||
if not _abandon_timer.is_armed():
|
||||
_map._abandon_timer.tick(delta)
|
||||
if not _map._abandon_timer.is_armed():
|
||||
return # 窗口到点回落(expired 信号已驱动 _reset_abandon_confirm)
|
||||
# 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪——档判据单点 motion_still)
|
||||
if ArmedTimer.motion_still():
|
||||
abandon_btn.modulate = Color(1, 1, 1, 0.75)
|
||||
else:
|
||||
var k := 0.5 + 0.5 * sin(_abandon_timer.elapsed * TAU)
|
||||
var k := 0.5 + 0.5 * sin(_map._abandon_timer.elapsed * TAU)
|
||||
abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k)
|
||||
|
||||
|
||||
# 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新)
|
||||
func open_choices(cands: Array, runner: LevelRunner) -> void:
|
||||
var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入)
|
||||
_runner = runner
|
||||
_map._runner = runner
|
||||
_hover_id = -1
|
||||
_reset_abandon_confirm()
|
||||
var visited: Array[int] = []
|
||||
if runner != null:
|
||||
visited = runner.visited_ids()
|
||||
if cands.size() > 1:
|
||||
_title.text = TITLE_FORK
|
||||
_map._title.text = TITLE_FORK
|
||||
elif visited.is_empty():
|
||||
_title.text = TITLE_ENTRY # 入口停点(尚未走出第一步)
|
||||
_map._title.text = TITLE_ENTRY # 入口停点(尚未走出第一步)
|
||||
else:
|
||||
_title.text = TITLE_SINGLE
|
||||
_map._title.text = TITLE_SINGLE
|
||||
_cand_ids = []
|
||||
for cand in cands:
|
||||
_cand_ids.append(_id_of(cand))
|
||||
_fit_canvas(_runner.nodes())
|
||||
_apply_level_tint(int(_runner.level_row.get("level_index", 0)))
|
||||
_fit_canvas(_map._runner.nodes())
|
||||
_apply_level_tint(int(_map._runner.level_row.get("level_index", 0)))
|
||||
visible = true
|
||||
if not was_visible:
|
||||
# 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定)
|
||||
@@ -302,7 +320,7 @@ func _fit_canvas(nodes: Array) -> void:
|
||||
_content_size = Vector2(LAYER_BASE_X + maxl * LAYER_STEP_X + NODE_HALF.x + 8.0,
|
||||
SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行
|
||||
_content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行)
|
||||
_canvas.custom_minimum_size = _content_size
|
||||
_map._canvas.custom_minimum_size = _content_size
|
||||
|
||||
|
||||
# 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的
|
||||
@@ -313,39 +331,39 @@ const BOARD_TINTS := {1: Color(1, 1, 1), 2: Color(0.84, 0.99, 1.28), 3: Color(0.
|
||||
|
||||
|
||||
func _apply_level_tint(level_index: int) -> void:
|
||||
if _noise_rect == null or level_index == _tint_level:
|
||||
if _map._layer_decor.noise_rect == null or level_index == _map._layer_decor.tint_level:
|
||||
return
|
||||
_tint_level = level_index
|
||||
_map._layer_decor.tint_level = level_index
|
||||
var target: Color = BOARD_TINTS.get(level_index, Color(1, 1, 1))
|
||||
var tw := create_tween()
|
||||
tw.tween_property(_noise_rect, "modulate",
|
||||
tw.tween_property(_map._layer_decor.noise_rect, "modulate",
|
||||
Color(target.r, target.g, target.b, UiKit.BACKDROP_TILE_ALPHA), UiKit.motion(0.45)) # 0.14 弱质感档随 tint 保持
|
||||
|
||||
|
||||
# 状态栏投喂(命令式纪律——组合根经 level_map_status_payload 取成品后喂入;零推送信号订阅、零核心态拼装)
|
||||
func apply_status(payload: Dictionary) -> void:
|
||||
_level_label.text = String(payload["level_label"])
|
||||
_gp_value.text = String(payload["gp_text"])
|
||||
_hp_value.text = String(payload["hp_text"])
|
||||
_map._level_label.text = String(payload["level_label"])
|
||||
_map._gp_value.text = String(payload["gp_text"])
|
||||
_map._hp_value.text = String(payload["hp_text"])
|
||||
if bool(payload["hp_bar_redraw"]):
|
||||
_hp_bar.queue_redraw()
|
||||
_attr_value.text = String(payload["attr_text"])
|
||||
_bonus_value.text = String(payload["bonus_text"])
|
||||
_map._hp_bar.queue_redraw()
|
||||
_map._attr_value.text = String(payload["attr_text"])
|
||||
_map._bonus_value.text = String(payload["bonus_text"])
|
||||
# 特性持有行可见性(与加成行并框——任一非空整框显示;文案归组合根 set_feat_holdings)
|
||||
_bonus_row.visible = bool(payload["bonus_row_visible"]) or _feat_value.text != ""
|
||||
_map._bonus_row.visible = bool(payload["bonus_row_visible"]) or _map._feat_value.text != ""
|
||||
|
||||
|
||||
# ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ----
|
||||
|
||||
func _on_canvas_input(ev: InputEvent) -> void:
|
||||
if _runner == null or not visible:
|
||||
if _map._runner == null or not visible:
|
||||
return
|
||||
if ev is InputEventMouseMotion:
|
||||
var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立)
|
||||
if nid != _hover_id:
|
||||
_hover_id = nid
|
||||
_canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW
|
||||
_canvas.queue_redraw()
|
||||
_map._canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW
|
||||
_map._canvas.queue_redraw()
|
||||
elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed:
|
||||
var nid2 := _candidate_at(ev.position)
|
||||
if nid2 >= 0:
|
||||
@@ -354,10 +372,10 @@ func _on_canvas_input(ev: InputEvent) -> void:
|
||||
|
||||
# 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应
|
||||
func _candidate_at(local_pos: Vector2) -> int:
|
||||
if _runner == null:
|
||||
if _map._runner == null:
|
||||
return -1
|
||||
var nodes: Array[Variant] = _runner.nodes()
|
||||
var pos := _layout(nodes, _canvas)
|
||||
var nodes: Array[Variant] = _map._runner.nodes()
|
||||
var pos := _layout(nodes, _map._canvas)
|
||||
for nid in _cand_ids:
|
||||
var node := _node_by_id(nid, nodes)
|
||||
if node.is_empty():
|
||||
@@ -372,7 +390,7 @@ func _candidate_at(local_pos: Vector2) -> int:
|
||||
func _submit_choice(nid: int) -> void:
|
||||
# 独立屏分支:战斗/Boss 节点 = 出屏切回战斗屏;奖励/恢复/商店节点 =
|
||||
# 本屏保持激活——兑付覆盖层由节点事件在本屏域内弹出(模态栈遮挡恢复语义复用),兑付确认后回图刷新
|
||||
var node := _node_by_id(nid, _runner.nodes())
|
||||
var node := _node_by_id(nid, _map._runner.nodes())
|
||||
var t := String(node.get("type", ""))
|
||||
if t != "reward" and t != "rest" and t != "shop":
|
||||
visible = false
|
||||
@@ -381,13 +399,13 @@ func _submit_choice(nid: int) -> void:
|
||||
|
||||
# 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造)
|
||||
func press_candidate(index: int) -> void:
|
||||
if index < 0 or index >= _cand_ids.size() or _runner == null:
|
||||
if index < 0 or index >= _cand_ids.size() or _map._runner == null:
|
||||
return
|
||||
var nodes: Array[Variant] = _runner.nodes()
|
||||
var nodes: Array[Variant] = _map._runner.nodes()
|
||||
var node := _node_by_id(int(_cand_ids[index]), nodes)
|
||||
if node.is_empty():
|
||||
return
|
||||
var pos: Vector2 = _layout(nodes, _canvas)[node]
|
||||
var pos: Vector2 = _layout(nodes, _map._canvas)[node]
|
||||
var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块)
|
||||
if nid >= 0:
|
||||
_submit_choice(nid)
|
||||
@@ -395,7 +413,7 @@ func press_candidate(index: int) -> void:
|
||||
|
||||
func _id_of(node: Dictionary) -> int:
|
||||
# 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth)
|
||||
return _runner.node_id_of(node)
|
||||
return _map._runner.node_id_of(node)
|
||||
|
||||
|
||||
# ---- 节点呈现决策面(信息迷雾 v2.1:类型仅在可见集内出示,Boss 标记不入雾)----
|
||||
@@ -421,20 +439,48 @@ static func node_display(node: Dictionary, nid: int, type_vis: Array,
|
||||
# ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)----
|
||||
|
||||
func _draw_graph(canvas: Control) -> void:
|
||||
if _runner == null or not visible:
|
||||
if _map._runner == null or not visible:
|
||||
return
|
||||
_drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面)
|
||||
var nodes: Array[Variant] = _runner.nodes()
|
||||
var pos := _layout(nodes, canvas)
|
||||
var font := get_theme_default_font() # 绘制路径字体单次取用(三处 draw_string 共享——零语义差)
|
||||
var current_id: int = _runner.current_node_id()
|
||||
# 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析
|
||||
var visited: Array[Variant] = _runner.visited_ids()
|
||||
var cand_ids: Array[Variant] = _runner.next_candidate_ids()
|
||||
var type_vis: Array[int] = _runner.type_visible_ids() # 本停点类型可见集(每帧一取——纯函数零状态)
|
||||
# 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)
|
||||
# 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的
|
||||
# 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象)
|
||||
_map._drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面)
|
||||
var ctx := _draw_context(canvas)
|
||||
_draw_passage_layer(canvas, ctx)
|
||||
for node in ctx["nodes"]:
|
||||
_draw_room(canvas, node, ctx)
|
||||
|
||||
|
||||
## 绘制上下文束(单次取数——布局/字体/编号空间/可见集,绘制域共享只读面)
|
||||
func _draw_context(canvas: Control) -> Dictionary:
|
||||
var nodes: Array[Variant] = _map._runner.nodes()
|
||||
return {
|
||||
"nodes": nodes,
|
||||
"pos": _layout(nodes, canvas),
|
||||
"font": get_theme_default_font(), # 绘制路径字体单次取用(三处 draw_string 共享——零语义差)
|
||||
"current_id": _map._runner.current_node_id(),
|
||||
# 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析
|
||||
"visited": _map._runner.visited_ids(),
|
||||
"cand_ids": _map._runner.next_candidate_ids(),
|
||||
"type_vis": _map._runner.type_visible_ids(), # 本停点类型可见集(每帧一取——纯函数零状态)
|
||||
}
|
||||
|
||||
|
||||
## 房间底色域(行进态三色:已到暗金/当前奖励金/未达暗——绘制域与图标域共享)
|
||||
func _room_color(node: Dictionary, ctx: Dictionary) -> Color:
|
||||
var nid := _id_of(node)
|
||||
var col := COL_ROOM_FUTURE
|
||||
if (ctx["visited"] as Array).has(nid):
|
||||
col = COL_ROOM_DONE
|
||||
if nid == ctx["current_id"]:
|
||||
col = COL_ROOM_CURRENT
|
||||
return col
|
||||
|
||||
|
||||
## 通道层绘制域:连线先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)。
|
||||
## 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的
|
||||
## 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象)
|
||||
func _draw_passage_layer(canvas: Control, ctx: Dictionary) -> void:
|
||||
var nodes: Array = ctx["nodes"]
|
||||
var pos: Dictionary = ctx["pos"]
|
||||
var cand_ids: Array = ctx["cand_ids"]
|
||||
var dim_passages: bool = not cand_ids.is_empty()
|
||||
for node in nodes:
|
||||
var from: Vector2 = pos[node]
|
||||
@@ -442,61 +488,74 @@ func _draw_graph(canvas: Control) -> void:
|
||||
var target := _node_by_id(tid, nodes)
|
||||
if target.is_empty():
|
||||
continue
|
||||
var to: Vector2 = pos[target]
|
||||
var lit: bool = _id_of(node) == current_id and cand_ids.has(tid)
|
||||
var hovered: bool = lit and _hover_id == tid
|
||||
var lit: bool = _id_of(node) == ctx["current_id"] and cand_ids.has(tid)
|
||||
var col := COL_PASSAGE_LIT if lit else (Color(COL_PASSAGE, 0.35) if dim_passages else COL_PASSAGE)
|
||||
var width := 4.0 if hovered else (3.0 if lit else 2.0) # 2/3/4 三档
|
||||
_draw_passage(canvas, from, to, col, width)
|
||||
# 房间方块
|
||||
for node in nodes:
|
||||
var p: Vector2 = pos[node]
|
||||
var t := String(node.get("type", ""))
|
||||
var nid := _id_of(node)
|
||||
var col := COL_ROOM_FUTURE
|
||||
if visited.has(nid):
|
||||
col = COL_ROOM_DONE
|
||||
if nid == current_id:
|
||||
col = COL_ROOM_CURRENT
|
||||
var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0)
|
||||
canvas.draw_rect(rect, col)
|
||||
var disp := node_display(node, nid, type_vis, nid == current_id, cand_ids.has(nid))
|
||||
var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false,
|
||||
"label_text": "", "boss_border": false}
|
||||
if bool(disp["boss_border"]):
|
||||
# Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制:
|
||||
# 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨)
|
||||
canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0)
|
||||
canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0)
|
||||
rec["boss_border"] = true
|
||||
elif cand_ids.has(nid):
|
||||
# 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行)
|
||||
canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0)
|
||||
# 节点图标(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色;
|
||||
# 贴图缺位 → 字形回退=贴图在位字形退役惯例)
|
||||
if bool(disp["draw_icon"]):
|
||||
rec["draw_icon"] = true
|
||||
var icon: Texture2D = _node_icons.get(t)
|
||||
if icon != null:
|
||||
var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0)
|
||||
canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0,
|
||||
Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint)
|
||||
else:
|
||||
var label := String(NODE_LABELS.get(t, "?"))
|
||||
canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE)
|
||||
# 编号(右上角——与图标占位区零重叠;双色墨随态)
|
||||
var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM
|
||||
canvas.draw_string(font, Vector2(rect.end.x - 16.0, rect.position.y + 8.0),
|
||||
str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink)
|
||||
# 类型文字标签(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、
|
||||
# 其余=类型名暖米弱化;文字+图标+形状三通道)
|
||||
if bool(disp["draw_label"]):
|
||||
rec["draw_label"] = true
|
||||
rec["label_text"] = String(disp["label_text"])
|
||||
var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT)
|
||||
canvas.draw_string(font, Vector2(p.x - 44.0, rect.end.y + 15.0),
|
||||
String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink)
|
||||
_drawn_disp[nid] = rec
|
||||
var width := 4.0 if lit and _hover_id == tid else (3.0 if lit else 2.0) # 2/3/4 三档
|
||||
_draw_passage(canvas, from, pos[target], col, width)
|
||||
|
||||
|
||||
## 房间方块域:底色随行进态+Boss/候选描边+图标/字形+编号+类型文字标签
|
||||
func _draw_room(canvas: Control, node: Dictionary, ctx: Dictionary) -> void:
|
||||
var pos: Dictionary = ctx["pos"]
|
||||
var p: Vector2 = pos[node]
|
||||
var t := String(node.get("type", ""))
|
||||
var nid := _id_of(node)
|
||||
var col := _room_color(node, ctx)
|
||||
var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0)
|
||||
canvas.draw_rect(rect, col)
|
||||
var disp := node_display(node, nid, ctx["type_vis"], nid == ctx["current_id"], (ctx["cand_ids"] as Array).has(nid))
|
||||
var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false,
|
||||
"label_text": "", "boss_border": false}
|
||||
if bool(disp["boss_border"]):
|
||||
# Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制:
|
||||
# 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨)
|
||||
canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0)
|
||||
canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0)
|
||||
rec["boss_border"] = true
|
||||
elif (ctx["cand_ids"] as Array).has(nid):
|
||||
# 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行)
|
||||
canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0)
|
||||
if _draw_room_icon(canvas, node, disp, ctx):
|
||||
rec["draw_icon"] = true
|
||||
# 编号(右上角——与图标占位区零重叠;双色墨随态)
|
||||
var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM
|
||||
canvas.draw_string(ctx["font"], Vector2(rect.end.x - 16.0, rect.position.y + 8.0),
|
||||
str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink)
|
||||
var label_rec := _draw_room_label(canvas, rect, disp, nid, ctx)
|
||||
rec["draw_label"] = bool(label_rec.get("draw_label", false))
|
||||
rec["label_text"] = String(label_rec.get("label_text", ""))
|
||||
_map._drawn_disp[nid] = rec
|
||||
|
||||
|
||||
## 节点图标域(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色;
|
||||
## 贴图缺位 → 字形回退=贴图在位字形退役惯例)
|
||||
func _draw_room_icon(canvas: Control, node: Dictionary, disp: Dictionary, ctx: Dictionary) -> bool:
|
||||
if not bool(disp["draw_icon"]):
|
||||
return false
|
||||
var p: Vector2 = ctx["pos"][node]
|
||||
var col := _room_color(node, ctx)
|
||||
var t := String(node.get("type", ""))
|
||||
var icon: Texture2D = _map._layer_decor.node_icons.get(t)
|
||||
if icon != null:
|
||||
var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0)
|
||||
canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0,
|
||||
Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint)
|
||||
else:
|
||||
var label := String(NODE_LABELS.get(t, "?"))
|
||||
canvas.draw_string(ctx["font"], p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE)
|
||||
return true
|
||||
|
||||
|
||||
## 类型文字标签域(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、
|
||||
## 其余=类型名暖米弱化;文字+图标+形状三通道)
|
||||
func _draw_room_label(canvas: Control, rect: Rect2, disp: Dictionary, nid: int, ctx: Dictionary) -> Dictionary:
|
||||
if not bool(disp["draw_label"]):
|
||||
return {}
|
||||
var p: Vector2 = rect.position + NODE_HALF # 节点中心反推(方块原点=中心−半宽高)
|
||||
var type_ink := COL_ROOM_CURRENT if nid == ctx["current_id"] else (UiKit.CANDLE if (ctx["cand_ids"] as Array).has(nid) else UiKit.WARM_FAINT)
|
||||
canvas.draw_string(ctx["font"], Vector2(p.x - 44.0, rect.end.y + 15.0),
|
||||
String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink)
|
||||
return {"draw_label": true, "label_text": String(disp["label_text"])}
|
||||
|
||||
|
||||
func _layout(nodes: Array, canvas: Control) -> Dictionary:
|
||||
@@ -520,20 +579,20 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary:
|
||||
|
||||
# ---- 探针只读出口(形态仿 fx_probe 先例:只读快照) ----
|
||||
|
||||
# 选路布局只读快照(node → 局部位置;画布固定取现行 _canvas,调用侧零私有域触达)
|
||||
# 选路布局只读快照(node → 局部位置;画布固定取现行 _map._canvas,调用侧零私有域触达)
|
||||
func layout_probe(nodes: Array) -> Dictionary:
|
||||
return _layout(nodes, _canvas)
|
||||
return _layout(nodes, _map._canvas)
|
||||
|
||||
|
||||
# 路由画布只读快照(origin=全局原点;min_size=最小拟合尺寸——画布拟合断言消费面)
|
||||
func canvas_probe() -> Dictionary:
|
||||
return {"origin": _canvas.get_global_position(), "min_size": _canvas.custom_minimum_size}
|
||||
return {"origin": _map._canvas.get_global_position(), "min_size": _map._canvas.custom_minimum_size}
|
||||
|
||||
|
||||
# 类型可见性绘制只读记录(id → 本帧实际绘制事实:图标/文字/描边/文案;_draw_graph 在实际绘制点写入——
|
||||
# 接线层断言消费点:绕开决策面直接画,这里就露出「雾下也画了」)
|
||||
func display_probe() -> Dictionary:
|
||||
return _drawn_disp
|
||||
return _map._drawn_disp
|
||||
|
||||
|
||||
# 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层
|
||||
@@ -577,7 +636,7 @@ func _entry(nodes: Array) -> Dictionary:
|
||||
|
||||
|
||||
func _out_ids(node: Dictionary, _nodes: Array) -> Array:
|
||||
return _runner.node_out_ids(node)
|
||||
return _map._runner.node_out_ids(node)
|
||||
|
||||
|
||||
func _node_by_id(nid: int, nodes: Array) -> Dictionary:
|
||||
|
||||
@@ -11,7 +11,7 @@ var _btn: Button
|
||||
|
||||
func _init() -> void:
|
||||
# M5(UI 落码批):rest 兑付面板 plate9 石板化+暖金头带字(demo ④休息弹层;暗字上亮板 §7.2)
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, true,
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14,
|
||||
UiKit.panel_skinned(Vector4(26, 18, 26, 18)))
|
||||
var box: VBoxContainer = shell.box
|
||||
_label = UiKit.label("",UiKit.FS_SUB, true, UiKit.INK_TITLE)
|
||||
|
||||
@@ -34,10 +34,18 @@ func _build() -> void:
|
||||
# 减动效与结算演出动画=同一设置,一行承载。
|
||||
# 两列对齐(label 列定宽 140——demo .setrow label/控制两列)+
|
||||
# 后果说明内联随行(demo .why 同排)+返回钮紧凑化(demo .btn9 紧凑)。
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, true,
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8,
|
||||
UiKit.panel_skinned(Vector4(20, 16, 20, 16)))
|
||||
var box: VBoxContainer = shell.box
|
||||
_build_head(box)
|
||||
_build_volume_row(box)
|
||||
_build_fullscreen_row(box)
|
||||
_build_motion_row(box)
|
||||
_build_close_row(box)
|
||||
|
||||
|
||||
## 头部域(设置项零增减——只有主音量;减动效与结算演出动画=同一设置,一行承载)
|
||||
func _build_head(box: VBoxContainer) -> void:
|
||||
var title := UiKit.label("设置",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(title)
|
||||
@@ -46,7 +54,9 @@ func _build() -> void:
|
||||
box.add_child(hint)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
# 音量(master bus 实时生效——入口归设置、生效口径归 音频.md)
|
||||
|
||||
## 音量行(master bus 实时生效——入口归设置、生效口径归 音频.md)
|
||||
func _build_volume_row(box: VBoxContainer) -> void:
|
||||
var vol_row := HBoxContainer.new()
|
||||
vol_row.add_theme_constant_override("separation", 14)
|
||||
box.add_child(vol_row)
|
||||
@@ -66,38 +76,44 @@ func _build() -> void:
|
||||
_volume_val.custom_minimum_size = Vector2(60, 0)
|
||||
vol_row.add_child(_volume_val)
|
||||
|
||||
# 全屏(拨杆形——SwitchToggle;状态字随行)
|
||||
|
||||
## 全屏行(拨杆形——SwitchToggle;状态字随行)
|
||||
func _build_fullscreen_row(box: VBoxContainer) -> void:
|
||||
var fs_row := HBoxContainer.new()
|
||||
fs_row.add_theme_constant_override("separation", 14)
|
||||
box.add_child(fs_row)
|
||||
var fs_lab := UiKit.label("全屏",UiKit.FS_BODY, false, UiKit.INK_LABEL)
|
||||
fs_lab.custom_minimum_size = Vector2(140, 0) # 同上
|
||||
fs_row.add_child(fs_lab)
|
||||
_fs_btn = UiKit.switch_toggle(false)
|
||||
_fs_btn = UiKit._switch_toggle(false)
|
||||
_fs_btn.toggled.connect(_on_fullscreen_toggled)
|
||||
fs_row.add_child(_fs_btn)
|
||||
_fs_st = UiKit.label("关",UiKit.FS_HINT, true, UiKit.INK_LABEL)
|
||||
fs_row.add_child(_fs_st)
|
||||
|
||||
# 结算演出动画(=减动效实称;拨杆 on=完整,off=快速时长减半。
|
||||
# reduced_motion 单设置同驱 UiKit.motion_scale 与结算演播——demo「减动效」行与
|
||||
# 本行系同一设置,不另立行(拆两行=一设置双行重复)
|
||||
|
||||
## 结算演出动画行(=减动效实称;拨杆 on=完整,off=快速时长减半。
|
||||
## reduced_motion 单设置同驱 UiKit.motion_scale 与结算演播——demo「减动效」行与
|
||||
## 本行系同一设置,不另立行(拆两行=一设置双行重复))
|
||||
func _build_motion_row(box: VBoxContainer) -> void:
|
||||
var motion_row := HBoxContainer.new()
|
||||
motion_row.add_theme_constant_override("separation", 14)
|
||||
box.add_child(motion_row)
|
||||
var motion_lab := UiKit.label("结算演出动画",UiKit.FS_BODY, false, UiKit.INK_LABEL)
|
||||
motion_lab.custom_minimum_size = Vector2(140, 0) # 同上
|
||||
motion_row.add_child(motion_lab)
|
||||
_motion_btn = UiKit.switch_toggle(true)
|
||||
_motion_btn = UiKit._switch_toggle(true)
|
||||
_motion_btn.toggled.connect(_on_motion_toggled)
|
||||
motion_row.add_child(_motion_btn)
|
||||
_motion_st = UiKit.label("完整",UiKit.FS_HINT, true, UiKit.INK_LABEL)
|
||||
motion_row.add_child(_motion_st)
|
||||
var why := UiKit.label("快速 = 时长减半(运动敏感 / 演示用)",UiKit.FS_CAPTION, false, UiKit.INK_HINT)
|
||||
motion_row.add_child(why) # 内联随行(demo .why 同排)
|
||||
|
||||
box.add_child(HSeparator.new())
|
||||
# 退出游戏归开始菜单——设置面不再承载(退出只在一级菜单出现)
|
||||
|
||||
|
||||
## 返回钮域(退出游戏归开始菜单——设置面不再承载〔退出只在一级菜单出现〕)
|
||||
func _build_close_row(box: VBoxContainer) -> void:
|
||||
var close_row := HBoxContainer.new()
|
||||
close_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(close_row)
|
||||
|
||||
@@ -10,7 +10,7 @@ var _log: RichTextLabel
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, true)
|
||||
var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, UiKit.PANEL_DEFAULT)
|
||||
(shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input)
|
||||
var box: VBoxContainer = shell.box
|
||||
box.add_child(UiKit.label("结算明细回看(完整流水 · 逐行归因)",UiKit.FS_SUB, true))
|
||||
|
||||
@@ -211,7 +211,7 @@ func _build_shelf_card(holder: Control, c: Dictionary) -> Dictionary:
|
||||
body = "%s %+d(本冒险)" % [String(KIND_LABELS.get(kind, kind)), int(c.get("value", 0))]
|
||||
var card := UiKit.bcard(String(c.get("display_name", kind)), body,
|
||||
UiKit.tg_band(kl), icon_key, # 类型色带单点(N4c/Q4 甲——货架与三选一同源判定,原固定默认带)
|
||||
"%d ◈" % int(c.get("price", 0)), String(KIND_LABELS.get(kind, kind)))
|
||||
{"price": "%d ◈" % int(c.get("price", 0)), "footnote": String(KIND_LABELS.get(kind, kind))})
|
||||
var btn := UiKit.button("购买")
|
||||
btn.pressed.connect(_emit_shelf_buy.bind(kind))
|
||||
card.add_footer(btn)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。
|
||||
## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。
|
||||
## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。
|
||||
## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出,
|
||||
## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar)
|
||||
## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。
|
||||
extends Node2D
|
||||
|
||||
const CHARGE_SKILL_IDS := ["heavy"]
|
||||
@@ -12,50 +15,68 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||||
# ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit)
|
||||
const STANDEE_FOOT_MARGIN := 12.0
|
||||
|
||||
var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入)
|
||||
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入)
|
||||
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界)
|
||||
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
|
||||
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
|
||||
|
||||
var _kind := "enemy"
|
||||
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
|
||||
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
|
||||
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
|
||||
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
|
||||
var _hero_tex := {}
|
||||
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 _hero_state: int = HeroState.IDLE
|
||||
var _state_revert_tw: Tween
|
||||
var _order := 1
|
||||
var _intent_glyph := ""
|
||||
var _intent_col := Color.WHITE
|
||||
var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据)
|
||||
var _intent_visible := false
|
||||
var _intent_scale := 1.0: # 意图弹入动画参数
|
||||
set(v):
|
||||
_intent_scale = v
|
||||
queue_redraw()
|
||||
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
|
||||
set(v):
|
||||
_glyph_rot = v
|
||||
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)
|
||||
queue_redraw()
|
||||
var _hp_tween: Tween
|
||||
var _hp_flash_tw: Tween
|
||||
|
||||
## 站桩演出状态(全部内部态单点——含 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 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
|
||||
|
||||
|
||||
func _init(kind: String = "enemy") -> void:
|
||||
_kind = kind
|
||||
st._owner = self
|
||||
st._hero_state = HeroState.IDLE
|
||||
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}
|
||||
|
||||
|
||||
func _efont() -> Font:
|
||||
@@ -63,26 +84,22 @@ func _efont() -> Font:
|
||||
|
||||
|
||||
func display_name() -> String:
|
||||
return _display_name
|
||||
return st._display_name
|
||||
|
||||
|
||||
# 敌单位身份出口(d12 站桩解析主键——同名敌按 id 精确匹配;主角站桩恒空串)
|
||||
func enemy_id() -> String:
|
||||
return _enemy_id
|
||||
|
||||
|
||||
# 血条存在性探针出口(世界直角血条常在)
|
||||
var has_world_hp_bar := true
|
||||
return st._enemy_id
|
||||
|
||||
|
||||
# 意图徽标探针出口
|
||||
func intent_probe() -> Dictionary:
|
||||
return {"visible": _intent_visible, "text": _intent_glyph}
|
||||
return {"visible": st._intent_visible, "text": st._intent_glyph}
|
||||
|
||||
|
||||
# 名字行探针出口
|
||||
func name_line_probe() -> Dictionary:
|
||||
return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))}
|
||||
return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))}
|
||||
|
||||
|
||||
# ---- setter 面(presenter/组合根零改排) ----
|
||||
@@ -90,35 +107,34 @@ func name_line_probe() -> Dictionary:
|
||||
# data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id,
|
||||
# pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)}
|
||||
func set_data(d: Dictionary) -> void:
|
||||
_display_name = String(d.get("display_name", "敌人"))
|
||||
_enemy_id = String(d.get("enemy_id", ""))
|
||||
_armor = String(d.get("armor_type", "light"))
|
||||
_hp_max = float(maxi(1, int(d.get("max_hp", 1))))
|
||||
st._display_name = String(d.get("display_name", "敌人"))
|
||||
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)))
|
||||
_set_order(int(d.get("order", 1)))
|
||||
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
|
||||
set_effects(d.get("effects", []))
|
||||
_refresh_portrait_glyph()
|
||||
if _dead and int(d.get("hp", 0)) > 0:
|
||||
_revive()
|
||||
queue_redraw()
|
||||
if st._dead and int(d.get("hp", 0)) > 0:
|
||||
_revive()
|
||||
|
||||
|
||||
func set_hero(hp: int, max_hp: int) -> void:
|
||||
_hp_max = float(maxi(1, max_hp))
|
||||
st._hp_max = float(maxi(1, max_hp))
|
||||
_set_hp(hp)
|
||||
if _dead and hp > 0:
|
||||
if st._dead and hp > 0:
|
||||
_revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义)
|
||||
|
||||
|
||||
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
|
||||
func _set_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 = create_tween()
|
||||
_hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))
|
||||
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(self, "st.hp_value", target, UiKit.motion(0.15))
|
||||
|
||||
|
||||
# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位)
|
||||
@@ -150,21 +166,21 @@ func set_effects_cleared_feedback() -> void:
|
||||
|
||||
func _set_order(i: int) -> void:
|
||||
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
|
||||
_order = i
|
||||
st._order = i
|
||||
queue_redraw()
|
||||
|
||||
|
||||
# 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;
|
||||
# "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。
|
||||
func set_pending(ptype: String, pid: String) -> void:
|
||||
if _dead:
|
||||
if st._dead:
|
||||
_intent_charge = false
|
||||
_intent_visible = false
|
||||
st._intent_visible = false
|
||||
queue_redraw()
|
||||
return
|
||||
if ptype == "" or ptype == "basic":
|
||||
_intent_charge = false
|
||||
_intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||||
st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||||
queue_redraw()
|
||||
return
|
||||
var glyph := "⚔" # 攻击系(damage)
|
||||
@@ -178,9 +194,9 @@ 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)
|
||||
_intent_glyph = glyph
|
||||
_intent_col = col
|
||||
_intent_visible = true
|
||||
st._intent_glyph = glyph
|
||||
st._intent_col = col
|
||||
st._intent_visible = true
|
||||
_play_intent_motion()
|
||||
|
||||
|
||||
@@ -189,17 +205,17 @@ func set_pending(ptype: String, pid: String) -> void:
|
||||
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
|
||||
func _play_intent_motion() -> void:
|
||||
var tw := 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))
|
||||
tw.tween_property(self, "st._intent_scale", 1.6, UiKit.motion(0.10))
|
||||
tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.16))
|
||||
tw.parallel().tween_property(self, "st._glyph_rot", 4.0, UiKit.motion(0.10))
|
||||
tw.tween_property(self, "st._glyph_rot", 0.0, UiKit.motion(0.14))
|
||||
if _intent_charge:
|
||||
tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
|
||||
tw.tween_property(self, "st._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))
|
||||
tw.tween_property(self, "st._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))
|
||||
tw.tween_property(self, "st._intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
|
||||
tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.13))
|
||||
|
||||
|
||||
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
|
||||
@@ -207,45 +223,45 @@ func _play_intent_motion() -> void:
|
||||
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。
|
||||
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
|
||||
if tex == null:
|
||||
if _portrait_sprite != null:
|
||||
_portrait_sprite.visible = false
|
||||
if st._portrait_sprite != null:
|
||||
st._portrait_sprite.visible = false
|
||||
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 # 像素帧放大防糊
|
||||
add_child(_portrait_sprite)
|
||||
_portrait_sprite.texture = tex
|
||||
_portrait_sprite.flip_h = flip
|
||||
_portrait_sprite.visible = true
|
||||
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 _portrait_sprite == null or _portrait_tex == null:
|
||||
if st._portrait_sprite == null or st._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)
|
||||
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)
|
||||
|
||||
|
||||
func mark_dead() -> void:
|
||||
if _dead:
|
||||
if st._dead:
|
||||
return
|
||||
_dead = true
|
||||
st._dead = true
|
||||
modulate = Color(0.62, 0.62, 0.62, 0.9)
|
||||
var tw := create_tween()
|
||||
tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12))
|
||||
tw.tween_property(self, "rotation_degrees", -1.5, UiKit.motion(0.1))
|
||||
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
|
||||
_spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s)
|
||||
_refresh_portrait_glyph()
|
||||
_intent_visible = false # 死亡残位意图退位
|
||||
_intent_charge = false
|
||||
st._intent_visible = false # 死亡残位意图退位
|
||||
_set_hp(0)
|
||||
if slot_row != null:
|
||||
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏
|
||||
@@ -253,37 +269,36 @@ func mark_dead() -> void:
|
||||
|
||||
# ---- 演出密度(程序化 tween,零新帧资产) ----
|
||||
|
||||
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
|
||||
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
|
||||
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
|
||||
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
|
||||
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
|
||||
|
||||
|
||||
# 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机)
|
||||
func _spawn_death_shards() -> void:
|
||||
_death_shards = DEATH_SHARD_N
|
||||
st._death_shards = DEATH_SHARD_N
|
||||
var center := _portrait_rect().get_center()
|
||||
for i in DEATH_SHARD_N:
|
||||
var shard := ColorRect.new()
|
||||
shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2)
|
||||
shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2)
|
||||
shard.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0
|
||||
shard.rotation = float(i) * 0.7
|
||||
add_child(shard)
|
||||
var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3))
|
||||
var peak := shard.position + dir
|
||||
var stw := shard.create_tween()
|
||||
stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
|
||||
stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27))
|
||||
stw.tween_callback(shard.queue_free)
|
||||
_spawn_death_shard(i, center)
|
||||
|
||||
|
||||
## 单碎块域(循环体提函数——弹道两段+渐隐+回收)
|
||||
func _spawn_death_shard(i: int, center: Vector2) -> void:
|
||||
var shard := ColorRect.new()
|
||||
shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2)
|
||||
shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2)
|
||||
shard.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0
|
||||
shard.rotation = float(i) * 0.7
|
||||
add_child(shard)
|
||||
var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3))
|
||||
var peak := shard.position + dir
|
||||
var stw := shard.create_tween()
|
||||
stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
|
||||
stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27))
|
||||
stw.tween_callback(shard.queue_free)
|
||||
|
||||
|
||||
# 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面)
|
||||
func fx_probe() -> Dictionary:
|
||||
return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count}
|
||||
return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count}
|
||||
|
||||
|
||||
# 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向
|
||||
@@ -292,65 +307,65 @@ func fx_probe() -> Dictionary:
|
||||
func play_hit_impact() -> void:
|
||||
var center := _portrait_rect().get_center()
|
||||
for i in HIT_SPARK_N:
|
||||
_hit_sparks += 1
|
||||
var spark := ColorRect.new()
|
||||
spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM
|
||||
spark.size = Vector2(4, 3)
|
||||
spark.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
spark.position = center - spark.size * 0.5
|
||||
add_child(spark)
|
||||
var ang := TAU * float(i) / float(HIT_SPARK_N)
|
||||
var dir := Vector2(cos(ang), sin(ang)) * 16.0
|
||||
var stw := spark.create_tween()
|
||||
stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12))
|
||||
stw.tween_callback(spark.queue_free)
|
||||
_spawn_hit_spark(i, center)
|
||||
|
||||
|
||||
## 单火花域(循环体提函数——放射弹道+渐隐+回收)
|
||||
func _spawn_hit_spark(i: int, center: Vector2) -> void:
|
||||
st._hit_sparks += 1
|
||||
var spark := ColorRect.new()
|
||||
spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM
|
||||
spark.size = Vector2(4, 3)
|
||||
spark.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
spark.position = center - spark.size * 0.5
|
||||
add_child(spark)
|
||||
var ang := TAU * float(i) / float(HIT_SPARK_N)
|
||||
var dir := Vector2(cos(ang), sin(ang)) * 16.0
|
||||
var stw := spark.create_tween()
|
||||
stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12))
|
||||
stw.tween_callback(spark.queue_free)
|
||||
|
||||
|
||||
func is_dead() -> bool:
|
||||
return _dead
|
||||
return st._dead
|
||||
|
||||
|
||||
# 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行)
|
||||
func flash_hit() -> void:
|
||||
_flash_count += 1
|
||||
if _hp_flash_tw != null and _hp_flash_tw.is_valid():
|
||||
_hp_flash_tw.kill()
|
||||
_hp_flash_tw = create_tween()
|
||||
_hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
|
||||
_hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2))
|
||||
st._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()
|
||||
st._hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
|
||||
st._hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not st._dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2))
|
||||
|
||||
|
||||
func _revive() -> void:
|
||||
_dead = false
|
||||
st._dead = false
|
||||
modulate = Color(1, 1, 1)
|
||||
rotation_degrees = 0.0
|
||||
if _kind == "hero":
|
||||
_hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
|
||||
if st._kind == "hero":
|
||||
st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
|
||||
set_hero_state("idle")
|
||||
_refresh_portrait_glyph()
|
||||
if slot_row != null:
|
||||
slot_row.visible = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _refresh_portrait_glyph() -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _portrait_glyph() -> String:
|
||||
# 字形回退仅作缺位兜底(八敌真图落位;相对体量语义由 48px 档统一比例承载)。
|
||||
# 缺位态:主角「人」/死亡「✕」/其余「敌」。
|
||||
if _kind == "hero":
|
||||
if st._kind == "hero":
|
||||
return "人"
|
||||
if _dead:
|
||||
if st._dead:
|
||||
return "✕"
|
||||
return "敌"
|
||||
|
||||
|
||||
# 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面)
|
||||
func portrait_probe() -> Dictionary:
|
||||
return {"has_tex": _portrait_sprite != null and _portrait_sprite.texture != null}
|
||||
return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null}
|
||||
|
||||
|
||||
# ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ----
|
||||
@@ -358,41 +373,41 @@ func portrait_probe() -> Dictionary:
|
||||
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
|
||||
func register_hero_state(state: String, tex: Texture2D) -> void:
|
||||
if tex != null:
|
||||
_hero_tex[state] = tex
|
||||
st._hero_tex[state] = tex
|
||||
|
||||
|
||||
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
|
||||
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
|
||||
func set_hero_state(state: String) -> void:
|
||||
var target: int = _HS_VALUE.get(state, -1)
|
||||
if _kind != "hero" or target == _hero_state:
|
||||
if st._kind != "hero" or target == st._hero_state:
|
||||
return
|
||||
if not _hero_tex.has(state) and state != "idle":
|
||||
if not st._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()
|
||||
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":
|
||||
_state_revert_tw = create_tween()
|
||||
_state_revert_tw.tween_interval(UiKit.motion(0.35))
|
||||
_state_revert_tw.tween_callback(_settle_hero_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)
|
||||
|
||||
|
||||
func _settle_hero_idle() -> void:
|
||||
_hero_state = HeroState.IDLE
|
||||
if _portrait_sprite != null and _hero_tex.has("idle"):
|
||||
_portrait_sprite.texture = _hero_tex["idle"]
|
||||
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——当前态名+注册态集合)
|
||||
func hero_state_probe() -> Dictionary:
|
||||
return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()}
|
||||
return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()}
|
||||
|
||||
|
||||
func _portrait_color() -> Color:
|
||||
return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP
|
||||
return UiKit.HERO_BLUE if st._kind == "hero" else UiKit.RED_DEEP
|
||||
|
||||
|
||||
# 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用
|
||||
@@ -413,8 +428,19 @@ func content_rect() -> Rect2:
|
||||
|
||||
func _draw() -> void:
|
||||
var f := _efont()
|
||||
# 椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
|
||||
# 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
|
||||
_draw_ground(f)
|
||||
_draw_hp_bar(f)
|
||||
_draw_portrait(f)
|
||||
_draw_intent_badge(f)
|
||||
if st._kind == "enemy":
|
||||
_draw_armor_name_line(f)
|
||||
|
||||
|
||||
## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
|
||||
## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
|
||||
## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族
|
||||
## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕)
|
||||
func _draw_ground(f: Font) -> void:
|
||||
var sh_center := Vector2(box.x * 0.5, box.y - 24.0)
|
||||
var sh_rx := (box.x - 36.0) * 0.5
|
||||
var sh_ry := 9.0
|
||||
@@ -423,28 +449,28 @@ func _draw() -> void:
|
||||
var ang := TAU * float(i) / 24.0
|
||||
pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry))
|
||||
draw_colored_polygon(pts, Color(0, 0, 0, 0.27))
|
||||
# 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族
|
||||
# ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕)
|
||||
var ped := _pedestal_rect()
|
||||
draw_rect(ped, UiKit.STAND_STONE, true)
|
||||
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
|
||||
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
|
||||
|
||||
# 血条(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8
|
||||
|
||||
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8;
|
||||
## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中;
|
||||
## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
|
||||
func _draw_hp_bar(f: Font) -> void:
|
||||
var bar_x := 8.0
|
||||
if _kind == "enemy":
|
||||
var order_txt := String("①②③"[clampi(_order - 1, 0, 2)])
|
||||
if st._kind == "enemy":
|
||||
var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)])
|
||||
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT)
|
||||
bar_x += 20.0
|
||||
var bar_w := box.x - 8.0 - bar_x
|
||||
draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移)
|
||||
var fill_w := bar_w * hp_value
|
||||
var fill_w := bar_w * st.hp_value
|
||||
if fill_w > 0.0:
|
||||
draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减)
|
||||
# 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中;
|
||||
# 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
|
||||
var hp_txt := "%d/%d" % [_hp_cur, int(_hp_max)]
|
||||
var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)]
|
||||
var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11)
|
||||
# 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2;
|
||||
# 中央垫 HP_BAR_BACK 深素牌 = 14.3:1;牌宽=字宽+8〔白名单取 8〕,高=条带全高;§4.1 数字色牌原则)
|
||||
@@ -453,46 +479,53 @@ func _draw() -> void:
|
||||
f.draw_string(get_canvas_item(), hbase + Vector2(1, 1), hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.10, 0.08, 0.06, 0.85))
|
||||
f.draw_string(get_canvas_item(), hbase, hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(1, 1, 1, 0.95))
|
||||
|
||||
# 立绘:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退
|
||||
if _portrait_sprite == null or _portrait_sprite.texture == null or not _portrait_sprite.visible:
|
||||
var prect := _portrait_rect()
|
||||
var glyph := _portrait_glyph()
|
||||
var center := prect.get_center()
|
||||
# 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的
|
||||
# pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移);
|
||||
# 传绝对 center 会成双重偏移、把字形画到盒外。
|
||||
draw_set_transform(center, deg_to_rad(_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())
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
|
||||
# 意图徽标(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
|
||||
if _intent_visible:
|
||||
var icenter := Vector2(box.x * 0.5, 86.0)
|
||||
draw_set_transform(icenter, 0.0, Vector2(_intent_scale, _intent_scale))
|
||||
var its := f.get_string_size(_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), _intent_glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, _intent_col) # 局部坐标(同上注记——双重偏移修复)
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
## 立绘域:贴图在位 = 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:
|
||||
return
|
||||
var prect := _portrait_rect()
|
||||
var glyph := _portrait_glyph()
|
||||
var center := prect.get_center()
|
||||
# 坐标系注记: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)
|
||||
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())
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
|
||||
# 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化)
|
||||
if _kind == "enemy":
|
||||
var armor_txt := String(ARMOR_ICONS.get(_armor, "◇"))
|
||||
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
|
||||
match _armor:
|
||||
"ward":
|
||||
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
|
||||
"heavy":
|
||||
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
|
||||
var name_w := f.get_string_size(_display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
|
||||
var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
|
||||
var total_w := armor_w + 3.0 + name_w
|
||||
var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律
|
||||
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
|
||||
f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), _display_name,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制
|
||||
|
||||
## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
|
||||
func _draw_intent_badge(f: Font) -> void:
|
||||
if not st._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)
|
||||
# 对比度垫牌(四色墨直压立绘纹理/暗舞台底 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)
|
||||
|
||||
|
||||
## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化)
|
||||
func _draw_armor_name_line(f: Font) -> void:
|
||||
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
|
||||
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
|
||||
match st._armor:
|
||||
"ward":
|
||||
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
|
||||
"heavy":
|
||||
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
|
||||
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
|
||||
var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
|
||||
var total_w := armor_w + 3.0 + name_w
|
||||
var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律
|
||||
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
|
||||
f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), st._display_name,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制
|
||||
@@ -14,7 +14,7 @@ var _feats_label: Label
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, true,
|
||||
var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10,
|
||||
UiKit.panel_skinned(Vector4(20, 16, 20, 16))) # 石板皮(缺位回退数据板——同设置面先例)
|
||||
var box: VBoxContainer = shell.box
|
||||
|
||||
|
||||
@@ -330,23 +330,9 @@ static func vignette_rect() -> TextureRect:
|
||||
# 程序纹理循环带,确定性生成零随机。光柱采用连续 alpha:
|
||||
# 带内 alpha 0→0.05→0 横向过渡+纵向衰减上亮下暗;
|
||||
# 纹理 filter=显式 LINEAR 白名单(与暗角/地图渐变同血统)
|
||||
static func beams_rect() -> TextureRect:
|
||||
static func _beams_rect() -> TextureRect:
|
||||
var img := Image.create(560, 560, false, Image.FORMAT_RGBA8)
|
||||
var ca := cos(0.2094) # 12°(102deg 带轴的垂直倾角)
|
||||
var sa := sin(0.2094)
|
||||
var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0)
|
||||
var band_lo := 240.0
|
||||
var band_hi := 340.0
|
||||
var band_mid := (band_lo + band_hi) * 0.5
|
||||
var band_peak := 0.05
|
||||
var band_half := (band_hi - band_lo) * 0.5
|
||||
for y in 560:
|
||||
var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha)
|
||||
for x in 560:
|
||||
var d := fposmod(x * ca + y * sa, 560.0)
|
||||
if d >= band_lo and d < band_hi:
|
||||
var a: float = band_peak * (1.0 - absf(d - band_mid) / band_half) * v_fall
|
||||
img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a))
|
||||
_paint_beams(img)
|
||||
var tr := TextureRect.new()
|
||||
tr.texture = ImageTexture.create_from_image(img)
|
||||
tr.stretch_mode = TextureRect.STRETCH_TILE
|
||||
@@ -356,8 +342,32 @@ static func beams_rect() -> TextureRect:
|
||||
return tr
|
||||
|
||||
|
||||
# 光带几何常量(12° 斜向光带——带轴倾角/带域/峰值强度;_paint_beams 单消费)
|
||||
const BEAMS_TILT_CA := 0.9135 # cos(0.2094)——12°(102deg 带轴的垂直倾角)
|
||||
const BEAMS_TILT_SA := 0.4067 # sin(0.2094)
|
||||
const BEAMS_BAND_LO := 240.0
|
||||
const BEAMS_BAND_HI := 340.0
|
||||
const BEAMS_PEAK := 0.05
|
||||
|
||||
|
||||
## 光带像素域(循环体提函数——12° 斜向光带双三角衰减:带内峰值×纵向衰减逐像素着色)
|
||||
static func _paint_beams(img: Image) -> void:
|
||||
var ca := BEAMS_TILT_CA
|
||||
var sa := BEAMS_TILT_SA
|
||||
var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0)
|
||||
var band_mid := (BEAMS_BAND_LO + BEAMS_BAND_HI) * 0.5
|
||||
var band_half := (BEAMS_BAND_HI - BEAMS_BAND_LO) * 0.5
|
||||
for y in 560:
|
||||
var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha)
|
||||
for x in 560:
|
||||
var d := fposmod(x * ca + y * sa, 560.0)
|
||||
if d >= BEAMS_BAND_LO and d < BEAMS_BAND_HI:
|
||||
var a: float = BEAMS_PEAK * (1.0 - absf(d - band_mid) / band_half) * v_fall
|
||||
img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a))
|
||||
|
||||
|
||||
# 竖向渐变底(demo #v-map linear-gradient(180deg, top 0%, low 40%)——stop 后恒 low)
|
||||
static func gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect:
|
||||
static func _gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect:
|
||||
var g := Gradient.new()
|
||||
g.offsets = PackedFloat32Array([0.0, stop])
|
||||
g.colors = PackedColorArray([top, low])
|
||||
@@ -442,7 +452,7 @@ static func label(text: String, size: int, bold: bool, color: Color = Color(-1,
|
||||
# PanelContainer(panel_sb 缺省 = 主题默认板——settlement_review 同款);box = 内容列
|
||||
#(min_size/separation 参数化,min_size ZERO = 不设)。返回 Dictionary{dim, center, panel, box}
|
||||
#——探针直读的私有件由调用方按名摘引,结构同名同层级不变。
|
||||
static func modal_dim(parent: Control, dim_color: Color) -> ColorRect:
|
||||
static func _modal_dim(parent: Control, dim_color: Color) -> ColorRect:
|
||||
var dim := ColorRect.new()
|
||||
dim.color = dim_color
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -450,18 +460,30 @@ static func modal_dim(parent: Control, dim_color: Color) -> ColorRect:
|
||||
return dim
|
||||
|
||||
|
||||
const PANEL_DEFAULT := true # modal_shell 面板哨兵(非 StyleBox 实例——null=无面板/本哨兵=默认样式盒面板/StyleBox=自定义)
|
||||
|
||||
|
||||
# 面板哨兵解析(六参收拢五参——三态:null=无面板〔原 with_panel=false〕/PANEL_DEFAULT=默认样式盒面板
|
||||
# 〔原 with_panel=true+panel_sb=null〕/StyleBox 实例=自定义样式盒面板〔原双参形态〕;
|
||||
# 判别用 is StyleBox——StyleBox 与布尔哨兵直接 != 属异类操作数违例)
|
||||
static func _resolve_shell_panel(panel_sb) -> PanelContainer:
|
||||
if panel_sb == null:
|
||||
return null
|
||||
var panel := PanelContainer.new()
|
||||
if panel_sb is StyleBox:
|
||||
panel.add_theme_stylebox_override("panel", panel_sb)
|
||||
return panel
|
||||
|
||||
|
||||
static func modal_shell(parent: Control, dim_color: Color, min_size := Vector2.ZERO, separation := 12,
|
||||
with_panel := false, panel_sb: StyleBox = null) -> Dictionary:
|
||||
var dim := modal_dim(parent, dim_color)
|
||||
panel_sb = null) -> Dictionary:
|
||||
var dim := _modal_dim(parent, dim_color)
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dim.add_child(center)
|
||||
var panel: PanelContainer = null
|
||||
var panel := _resolve_shell_panel(panel_sb)
|
||||
var host: Control = center
|
||||
if with_panel:
|
||||
panel = PanelContainer.new()
|
||||
if panel_sb != null:
|
||||
panel.add_theme_stylebox_override("panel", panel_sb)
|
||||
if panel != null:
|
||||
center.add_child(panel)
|
||||
host = panel
|
||||
var box := VBoxContainer.new()
|
||||
@@ -563,19 +585,6 @@ static func panel_skinned(margin: Vector4 = Vector4.ZERO, tint := Color.WHITE) -
|
||||
|
||||
|
||||
# 仪式石框(空心角括框——营地容器框;缺位回退暗面板)
|
||||
static func frame_skinned(margin: Vector4 = Vector4.ZERO) -> StyleBox:
|
||||
var tex := load_tex(TEX_BORDER_STONE)
|
||||
if tex == null:
|
||||
return panel_flat(BG_OVERLAY, PANEL_BORDER,RADIUS_OBJECT, 1, margin)
|
||||
var sb := _tex_sb(tex, 12.0)
|
||||
sb.content_margin_left = margin.x
|
||||
sb.content_margin_top = margin.y
|
||||
sb.content_margin_right = margin.z
|
||||
sb.content_margin_bottom = margin.w
|
||||
return sb
|
||||
|
||||
|
||||
# 图标槽(营地钥石/柜台件——缺位返回 null,消费点不挂 TextureRect 即回退纯文本)
|
||||
static func icon_rect(tex_path: String, px: float) -> TextureRect:
|
||||
var t := load_tex(tex_path)
|
||||
if t == null:
|
||||
@@ -659,19 +668,6 @@ static func _apply_button_skin(b: Button) -> void:
|
||||
# 消费方 = 标题/营地/设置/商店/三选一屏。单点纪律同 §7.1:新件只加不改旧件)──
|
||||
|
||||
# 动词按钮(图标+动词+悬停说明三件套——标题屏按钮组/通用;icon 缺位=纯文字不炸)
|
||||
static func verb_button(text: String, icon_path: String, tip := "") -> Button:
|
||||
var b := button(text)
|
||||
var tex := load_tex(icon_path)
|
||||
if tex != null:
|
||||
b.icon = tex
|
||||
b.add_theme_constant_override("icon_max_width", 20)
|
||||
b.expand_icon = false
|
||||
if tip != "":
|
||||
b.tooltip_text = tip
|
||||
return b
|
||||
|
||||
|
||||
# 主操作钮(烛光黄平涂+木描边——非奖励金;配色总纲「主按钮禁亮金」;缺 panel_stone 依赖=纯色可靠)
|
||||
static func primary_button(text: String, tip := "") -> Button:
|
||||
var b := button(text)
|
||||
var sb := panel_flat(CANDLE, Color8(110, 69, 52),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2))
|
||||
@@ -749,7 +745,9 @@ class BCard:
|
||||
var _sb_normal: StyleBox
|
||||
var _sb_sel: StyleBox
|
||||
|
||||
func _init(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> void:
|
||||
func _init(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> void:
|
||||
var price: String = opts.get("price", "")
|
||||
var footnote: String = opts.get("footnote", "")
|
||||
var tex := UiKit.load_tex(UiKit.TEX_PANEL_STONE)
|
||||
_sb_normal = UiKit.panel_skinned(Vector4(12, 10, 12, 10)) if tex != null else UiKit.panel_flat(PANEL_DATA, PANEL_BORDER,RADIUS_OBJECT, 1, Vector4(12, 10, 12, 10)) # 回退=数据板单点
|
||||
_sb_sel = UiKit.panel_flat(PANEL_DATA, Color8(138, 90, 52), RADIUS_OBJECT, 2, Vector4(12, 10, 12, 10)) # 选中底=数据板单点
|
||||
@@ -766,21 +764,7 @@ class BCard:
|
||||
mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
mark.visible = false
|
||||
box.add_child(mark)
|
||||
# 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍)
|
||||
var slot := PanelContainer.new()
|
||||
slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)) # 明度差降档(柔和档;白图标 ≥3:1 硬约束在役)
|
||||
slot.custom_minimum_size = Vector2(76, 76)
|
||||
slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
box.add_child(slot)
|
||||
icon_tr = TextureRect.new()
|
||||
var itex := UiKit.load_tex(icon_path)
|
||||
if itex != null:
|
||||
icon_tr.texture = itex
|
||||
icon_tr.custom_minimum_size = Vector2(60, 60)
|
||||
icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
slot.add_child(icon_tr)
|
||||
box.add_child(_build_icon_slot(icon_path))
|
||||
name_l = UiKit.label(title,FS_SUB, true, INK_TITLE)
|
||||
name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(name_l)
|
||||
@@ -800,6 +784,23 @@ class BCard:
|
||||
foot_l.visible = footnote != ""
|
||||
box.add_child(foot_l)
|
||||
|
||||
# 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍;明度差降档——柔和档;白图标 ≥3:1 硬约束在役)
|
||||
func _build_icon_slot(icon_path: String) -> PanelContainer:
|
||||
var slot := PanelContainer.new()
|
||||
slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1))
|
||||
slot.custom_minimum_size = Vector2(76, 76)
|
||||
slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
icon_tr = TextureRect.new()
|
||||
var itex := UiKit.load_tex(icon_path)
|
||||
if itex != null:
|
||||
icon_tr.texture = itex
|
||||
icon_tr.custom_minimum_size = Vector2(60, 60)
|
||||
icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
slot.add_child(icon_tr)
|
||||
return slot
|
||||
|
||||
func set_selected(v: bool) -> void:
|
||||
add_theme_stylebox_override("panel", _sb_sel if v else _sb_normal)
|
||||
mark.visible = v
|
||||
@@ -809,8 +810,9 @@ class BCard:
|
||||
body.move_child(c, foot_l.get_index())
|
||||
|
||||
|
||||
static func bcard(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> BCard:
|
||||
return BCard.new(title, desc, band, icon_path, price, footnote)
|
||||
## 竖卡工厂(BCard._init 同形收拢 6→5 参——opts 键 {price, footnote} 缺省逐字同值)
|
||||
static func bcard(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> BCard:
|
||||
return BCard.new(title, desc, band, icon_path, opts)
|
||||
|
||||
|
||||
# 拨杆开关(设置屏——46×22 滑柄形;状态字随行由消费方摆;会话级/持久由消费方接 SettingsStore)
|
||||
@@ -838,7 +840,7 @@ class SwitchToggle:
|
||||
_knob.size = Vector2(18, 16)
|
||||
|
||||
|
||||
static func switch_toggle(initial: bool) -> SwitchToggle:
|
||||
static func _switch_toggle(initial: bool) -> SwitchToggle:
|
||||
return SwitchToggle.new(initial)
|
||||
|
||||
|
||||
|
||||
@@ -104,13 +104,13 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
var iso = SaveManagerScript.new(save_path)
|
||||
iso.load_from_disk()
|
||||
if mark_first_done:
|
||||
iso.mark_first_adventure_done()
|
||||
iso.meta_state.mark_first_adventure_done()
|
||||
# --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
if swap_save:
|
||||
sm.attach_path(save_path)
|
||||
sm.slots.attach_path(save_path)
|
||||
if mark_first_done:
|
||||
sm.mark_first_adventure_done()
|
||||
sm.meta_state.mark_first_adventure_done()
|
||||
iso = sm
|
||||
else:
|
||||
main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动
|
||||
@@ -123,10 +123,10 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
# 驱动后写盘目标=隔离槽文件(持久化断言读 user://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
|
||||
static func open_camp_via_savesel(main, tag: String) -> bool:
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
sm.attach_slot_template("user://%s_slot_%%d.json" % tag)
|
||||
sm.slots.attach_slot_template("user://%s_slot_%%d.json" % tag)
|
||||
for i in range(1, 4): # 夹具清场——空槽起(陈旧槽文件会翻有档态、新档钮缺席)
|
||||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||||
var sp: String = sm.slot_path(i) + suf
|
||||
var sp: String = sm.slots.slot_path(i) + suf
|
||||
if FileAccess.file_exists(sp):
|
||||
DirAccess.remove_absolute(sp)
|
||||
main._open_save_select()
|
||||
|
||||
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user