合并 master(迭代39 拆件批+文档批) 进 iter40+主角账模型迁移适配——冲突两件按「保留本分支拆件结构+承接 master 语义」解:①battle_orchestrator 承 RunLedger 整类退役=新增 _journal()/_attune_loadout() 两读面(enc 派生、无成员接收)、_reseed_attune_charges 循环体换 AttuneLoadout.reset_cycle(写时种行/补行单源化)、冒险桶挂接改 enc["adv_bucket"](经 _fx_host.mount_buckets 下传)、战末 bonus 汇入换 GainJournal.gain_bonus(axis,value,"battle_export",axis);同时接回 master 的 B4 战斗指标面=_mines_hits 三读数账+开战清零+轮末转存、_roll_round_actions facts 组包四键(round_index/last_round_end/mines_hit_last/mines_hit_total)并传 check_trigger_unlocks/rollable_skills/special_skills 三参、enemy_hp_threshold 读数改 trigger.value。②RewardGrant 的 battle_ledger 引用改 battle_hooks 回调束三键(attune_consume/vision_add/negative_blast,bind_battle 缺键 assert)——授予件对账本零强引用、宿主侧单点留编排器,类变量封闭面 15 不减不增;use_treasure 充能消耗与真视计数改经回调,负向 blast 踩雷计数在 _flat_blast 判极点后回调宿主记账(等价承 master 的 _book_negative_blast 语义)。③treasure_effects_test 计数链断言取 master 口径(lg13.vision_used()==1)+本分支真实入口(reward_grant().use_treasure)。④trigger_cond_test V6 的两处 flat 行调用改指 reward_grant()._apply_flat_row(att 束形)——它原调编排器已迁函数触发 Invalid call,_init 在 quit 之前中断=进程挂死 124 超时(本轮排查实证:同件在 master rc=0 秒退,合并后挂死;修后 rc=0 全绿)。验收:run_all 65 套件 64 绿 1 红=已登记存量红(02 EV 锚 goblin 0.9148∉[1.00,1.20],迭代39 特批带红在册,重锚归后续数值批),零 SCRIPT ERROR;负向套件 run_time_fatal_test exit=1 三点绿;variant_placement 49 checks 绿(wr10 三条在位);gdstyle 待合并后复测〔结构小件批 V16〕
This commit is contained in:
commit
b8b69c683d
75 files changed
+1756019
-2753
No files matched your search
+234
-563
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,117 @@
|
||||
## 同调配装件:装备中的秘宝件对象集唯一所有者(每件内含充能单位与重置周期——冒险域拆分姊妹批自共享充能账字典轨迁移)。
|
||||
## 同调充能单源化:物化行(treasure_inventory 内 source_class="attunement" 行)为使用面唯一事实源——
|
||||
## 剩余次数由行的未用数派生(唯一读法,无影子账);字典轨与三处事后差额对账退役,改「写时种行/补行」。
|
||||
## 种行/补行写时同步:冒险开始全量种行(不筛周期——battle/level 周期件开局即有行,C 区投影行数不变);
|
||||
## battle/level 周期回满走 reset_cycle(已耗 handle 不回收=作废、差额补新——语义逐字承原编排器重置);
|
||||
## 同调未用行跨关存活(关卡域库存提为冒险级共享、关末清空改选择性保留——单源化的行承载前提)。
|
||||
## 旧档读侧以行集为源恢复使用面,attunement_charges 键读值仅作一致性校验读数(不符记日志不 fatal——键值本就派生自行)。
|
||||
## Core 层零 UI;零随机(充能域无 RNG 消费);随机无关、数值只读 db。
|
||||
class_name AttuneLoadout
|
||||
extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var _ids: Array[Variant] = [] # 装配集(活引用注入——宿主 start/resume 时点下传)
|
||||
|
||||
func _init(db_ref: ConfigDb, attunement_ids: Array) -> void:
|
||||
db = db_ref
|
||||
set_attunement(attunement_ids)
|
||||
|
||||
|
||||
# 装配集注入(活引用——宿主 start_adventure/resume 参数面锁定后下传)
|
||||
func set_attunement(attunement_ids: Array) -> void:
|
||||
_ids = attunement_ids
|
||||
|
||||
|
||||
func item_ids() -> Array:
|
||||
return _ids.duplicate()
|
||||
|
||||
|
||||
# ---- 写时种行/补行(替代三处事后差额对账)----
|
||||
|
||||
# 冒险开始:全部 active_charged 件种行(**不筛周期**——承现 _seed_attune_charges 全量种账+sync 全量投行
|
||||
# 语义;battle/level 周期件开局即有行,C 区投影行数不变)
|
||||
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)
|
||||
if String(arow.get("form", "")) != "active_charged":
|
||||
continue
|
||||
_ensure_rows(iid, maxi(0, int(arow.get("charges_per_adventure", 0))), inventory)
|
||||
|
||||
|
||||
# battle/level 周期回满(**按周期筛**;已耗行作废、差额补新——语义逐字承原编排器 _reseed_attune_charges;
|
||||
# charges_per_adventure=0=自动兑付件不入使用面〔试炼镐——不走骑库存面〕)
|
||||
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)
|
||||
if String(arow.get("form", "")) != "active_charged":
|
||||
continue
|
||||
if String(arow.get("charge_reset", "adventure")) != cycle:
|
||||
continue
|
||||
var want := int(arow.get("charges_per_adventure", 0))
|
||||
if want < 1:
|
||||
continue # charges=0=自动兑付件(不走骑库存面,兑付=积点段自动 grant_ap)
|
||||
_ensure_rows(iid, want, inventory)
|
||||
|
||||
|
||||
# 差额补行单点(未用行数不足 want 即补——「差额注入防续玩双份」现役语义逐字)
|
||||
func _ensure_rows(iid: String, want: int, inventory: TreasureItems) -> void:
|
||||
var existing := 0
|
||||
for r in inventory.snapshot_inventory():
|
||||
if String(r.item_id) == iid and String(r.source_class) == "attunement" and not bool(r.used):
|
||||
existing += 1
|
||||
for i in range(maxi(0, want - existing)):
|
||||
inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0)
|
||||
|
||||
|
||||
# ---- 使用面(战斗路径改指)----
|
||||
|
||||
# 充能消耗(承原 attune_charge_consume「扣账 -1」语义——单源化后=行标 used 幂等承载:
|
||||
# 使用管线 consume 句柄已标行 used,本口兜底标未用行〔双入口同点防御——行 used 不可逆幂等〕)
|
||||
func consume_charge(item_id: String, inventory: TreasureItems) -> void:
|
||||
for r in inventory.snapshot_inventory():
|
||||
if String(r.item_id) == item_id and String(r.source_class) == "attunement" and not bool(r.used):
|
||||
inventory.consume(int(r.handle)) # 行标 used(与使用管线 consume 同口——幂等兜底)
|
||||
return
|
||||
|
||||
|
||||
# ---- 读面(行派生——唯一读法)----
|
||||
|
||||
# 剩余次数(未用行数派生——原字典轨读数逐位等价)
|
||||
func charges_left(item_id: String, inventory: TreasureItems) -> int:
|
||||
var n := 0
|
||||
for r in inventory.snapshot_inventory():
|
||||
if String(r.item_id) == item_id and String(r.source_class) == "attunement" and not bool(r.used):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件——键名与策略不变)----
|
||||
|
||||
# attunement_charges 键(值形状=原字典轨 {item_id:剩余}——由行派生,键名/策略不变)
|
||||
func breakpoint_fragment(inventory: TreasureItems) -> Dictionary[Variant, Variant]:
|
||||
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)
|
||||
if String(arow.get("form", "")) == "active_charged":
|
||||
charges[iid] = charges_left(iid, inventory)
|
||||
return {"attunement_charges": charges}
|
||||
|
||||
|
||||
# 旧档读侧(以行集为源恢复使用面——键值仅校验读数:不符记日志不 fatal;缺键保持种子态=原语义)。
|
||||
# 调用时点=行集已随 aux_items 回填(恢复链序:库存回填先于本口)。
|
||||
func restore_fragment(frag: Dictionary, inventory: TreasureItems) -> void:
|
||||
var stored: Variant = frag.get("attunement_charges", null)
|
||||
if not (stored is Dictionary) or (stored as Dictionary).is_empty():
|
||||
return # 缺键 = 旧档/无断点,保持种子态(原语义)
|
||||
for iid in (stored as Dictionary).keys():
|
||||
var derived := charges_left(String(iid), inventory)
|
||||
var recorded := int((stored as Dictionary)[iid])
|
||||
if derived != recorded:
|
||||
# 跨版续玩兜底:改前档关卡完成停点的同调行已被关末清空(键在行缺)——按键值差额补行,
|
||||
# 使用面与改前字典轨续玩逐位等价;新档行集为源时差额=0 零操作(行集仍为读数唯一来源)
|
||||
push_warning("[同调配装] 充能校验读数与行派生不一致(%s 档=%d 行派生=%d——按键值差额补行)"
|
||||
% [String(iid), recorded, derived])
|
||||
_ensure_rows(String(iid), recorded, inventory)
|
||||
@@ -0,0 +1 @@
|
||||
uid://24ntqf46dfec
|
||||
@@ -76,7 +76,8 @@ var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换
|
||||
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
|
||||
# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
|
||||
var attunement: Array[Variant] = [] # 装备中同调件 item_id 集
|
||||
var _ledger: RunLedger = null # 共享四本账(begin_battle 经 enc["ledger"] 持引用;缺省新账=调试面)
|
||||
var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账)
|
||||
# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
|
||||
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口)
|
||||
var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
|
||||
# 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
|
||||
@@ -84,6 +85,45 @@ var nest_preview_count := -1 # 巢穴预告读数口(board_insta
|
||||
var _feat_first_dig_used := false
|
||||
|
||||
|
||||
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读;
|
||||
# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账)
|
||||
func _journal() -> GainJournal:
|
||||
return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null
|
||||
|
||||
|
||||
# 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge
|
||||
# 消费面;null=直构零装配,充能面跳过)
|
||||
func _attune_loadout() -> AttuneLoadout:
|
||||
return encounter.get("loadout", null)
|
||||
|
||||
|
||||
# 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数;
|
||||
# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用)
|
||||
func _battle_hooks() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"attune_consume": _hook_attune_consume,
|
||||
"vision_add": _hook_vision_add,
|
||||
"negative_blast": _book_negative_blast,
|
||||
}
|
||||
|
||||
|
||||
func _hook_attune_consume(item_id: String) -> void:
|
||||
if _attune_loadout() != null and treasure_inventory != null:
|
||||
_attune_loadout().consume_charge(item_id, treasure_inventory)
|
||||
|
||||
|
||||
func _hook_vision_add() -> void:
|
||||
if _journal() != null:
|
||||
_journal().record_vision_use() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义)
|
||||
|
||||
|
||||
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
|
||||
# 极性判定在授予件 _flat_blast 传点,本口只记账)
|
||||
func _book_negative_blast() -> void:
|
||||
_mines_hits["round"] += 1
|
||||
_mines_hits["total"] += 1
|
||||
|
||||
|
||||
# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null)
|
||||
func _fx_host_fact(fact: String) -> Variant:
|
||||
match fact:
|
||||
@@ -160,24 +200,9 @@ func _attune_charge_safe_row() -> Dictionary:
|
||||
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
|
||||
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
|
||||
func _reseed_attune_charges(reset: String) -> void:
|
||||
for iid_v in attunement:
|
||||
var iid := String(iid_v)
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
|
||||
if String(arow.get("form", "")) != "active_charged":
|
||||
continue
|
||||
if String(arow.get("charge_reset", "adventure")) != reset:
|
||||
continue
|
||||
var want := int(arow.get("charges_per_adventure", 0))
|
||||
if want < 1:
|
||||
continue # charges=0=自动兑付件(试炼镐——不走骑库存面,兑付=_on_cell_revealed 积点段自动 grant_ap)
|
||||
_ledger.attune_charge_set(iid, want)
|
||||
if treasure_inventory != null:
|
||||
var existing := 0
|
||||
for r in treasure_inventory.snapshot_inventory():
|
||||
if String(r.get("item_id", "")) == iid and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)):
|
||||
existing += 1
|
||||
for i in range(maxi(0, want - existing)):
|
||||
treasure_inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0)
|
||||
if _attune_loadout() == null or treasure_inventory == null:
|
||||
return # 直构零装配/库存未就位——充能回满跳过(空装配同语义)
|
||||
_attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
|
||||
|
||||
|
||||
# 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行:
|
||||
@@ -335,10 +360,10 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
|
||||
attunement = (enc.get("attunement", []) as Array).duplicate()
|
||||
_attune_safe_seen = 0
|
||||
_fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1)))
|
||||
var ledger_ref = enc.get("ledger", null)
|
||||
_ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面
|
||||
_fx_host.mount_buckets(_ledger.adventure_counters, enc.get("level_counters")) # 挂冒险桶引用(跨战持久=同一字典引用,非副本搬运);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
|
||||
_reward_grant.bind_battle(_ledger)
|
||||
_mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
|
||||
var adv_bucket: Variant = enc.get("adv_bucket", null)
|
||||
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
|
||||
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
|
||||
_damage.fx_calls["reveal"] = _fx_host.enqueue_reveal
|
||||
_damage.fx_calls["ac"] = _fx_host.apply_ac_bonus
|
||||
var feat_ids: Array[Variant] = enc.get("feats", []) as Array
|
||||
@@ -653,22 +678,31 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
# 摇定后、兑现前死亡 → 本拍技能作废。
|
||||
func _roll_round_actions() -> void:
|
||||
var pity_m: int = int(db.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")),
|
||||
"mines_hit_last": int(_mines_hits["last"]),
|
||||
"mines_hit_total": int(_mines_hits["total"]),
|
||||
}
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
|
||||
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
|
||||
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
|
||||
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
|
||||
for sid in _enemy_system.check_skill_unlocks(u):
|
||||
for sid in _enemy_system.check_trigger_unlocks(u, facts):
|
||||
var srow_u: Dictionary[Variant, Variant] = db.skill_row(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})
|
||||
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
|
||||
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值)
|
||||
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
|
||||
var pct_v := 0
|
||||
for pe in (u.get("pool", []) as Array):
|
||||
if String((pe as Dictionary).get("skill_id", "")) == sid and (pe as Dictionary).has("unlock_hp_pct"):
|
||||
pct_v = int((pe as Dictionary).unlock_hp_pct)
|
||||
var pe_d: Dictionary[Variant, Variant] = pe
|
||||
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
|
||||
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
|
||||
break
|
||||
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
|
||||
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
|
||||
@@ -677,14 +711,16 @@ func _roll_round_actions() -> void:
|
||||
for h in eht_hits:
|
||||
if String(h.get("kind", "")) == "target_attack_bonus":
|
||||
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
|
||||
var entries: Array[Variant] = _enemy_system.rollable_skills(u)
|
||||
var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts)
|
||||
if _enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array[Variant] = _enemy_system.special_skills(u)
|
||||
var specials: Array[Variant] = _enemy_system.special_skills(u, facts)
|
||||
if not specials.is_empty():
|
||||
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
|
||||
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
|
||||
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
|
||||
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
|
||||
_mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
|
||||
_mines_hits["round"] = 0
|
||||
|
||||
|
||||
# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd
|
||||
@@ -810,11 +846,11 @@ func _battle_over(result: String) -> void:
|
||||
if String(h.get("kind", "")) == "heal":
|
||||
var hu_w: Dictionary[Variant, Variant] = _enemy_system.hero_unit()
|
||||
hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1))))
|
||||
# bonus_delta 战末汇入(血税双向账:本战累积并入冒险加算区共享引用后清零)+
|
||||
# bonus_delta 战末汇入(血税双向账:本战累积经加算日记单写原语汇入获得日记三层后清零)
|
||||
var drained := _fx_host.bonus_delta_drain()
|
||||
if not _ledger.bonus.is_empty():
|
||||
if _journal() != null:
|
||||
for axis in drained.keys():
|
||||
_ledger.bonus_add_axis(axis, int(drained[axis]))
|
||||
_journal().gain_bonus(String(axis), int(drained[axis]), "battle_export", String(axis))
|
||||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||||
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||||
var gp_dropped := _roll_gp_drop() # 战斗掉落(escrow 随此值上抛即弃)
|
||||
|
||||
@@ -118,12 +118,9 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
|
||||
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
|
||||
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
|
||||
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
|
||||
# 施放类 side 守卫(buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接)
|
||||
func _pool_cast_side_errs(skill_table: SkillTable, status_table: StatusEffectTable) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skill_types := {}
|
||||
for row in skill_table.rows:
|
||||
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
|
||||
# 施放类 side 守卫:buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接
|
||||
for row in skill_table.rows:
|
||||
var st: String = String(row.get("type", ""))
|
||||
if st != "buff" and st != "debuff":
|
||||
@@ -133,11 +130,21 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
|
||||
var srow: Dictionary[Variant, Variant] = status_table.row_by_id(eff) if status_table != null else {}
|
||||
if not srow.is_empty() and String(srow.get("target_side", "")) != want_side:
|
||||
errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]})
|
||||
return errs
|
||||
|
||||
|
||||
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skill_types := {}
|
||||
for row in skill_table.rows:
|
||||
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
|
||||
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
|
||||
for entry in row.get("pool", []):
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
@@ -150,10 +157,17 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
|
||||
buff_n += 1
|
||||
"debuff":
|
||||
debuff_n += 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():
|
||||
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:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池无条件特殊技行数 = 0——每池至少一行无条件特殊技纪律(触发条件批 2026-10-26)" % [eid]})
|
||||
return errs
|
||||
+2
-23
@@ -1,7 +1,6 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 令牌池件:三选一/商店共享池的持久多重集、抽签、库存与专长凭证判定唯一所有者(自 adventure_runner 拆出,零行为变更)。
|
||||
## 停点编排与兑付推进不在此件(留冒险编排器)——件只管池与判定;随机只消费注入的 RngServiceCore(reward_pick 封闭流)。
|
||||
## 宿主兑付单点经 bind_handlers 注入方法 Callable(选中摘牌后的加算区/账本落点仍在宿主)。
|
||||
## 停点编排与兑付推进归 CurioStop 件——件只管池与判定(纯池化:反向回调退役);随机只消费注入的 RngServiceCore(reward_pick 封闭流)。
|
||||
class_name CurioDeck
|
||||
extends RefCounted
|
||||
|
||||
@@ -12,8 +11,6 @@ var taken: Dictionary[Variant, Variant] = {} # 件获取次数计
|
||||
var ticket_pity := 0 # 专长凭证保底计数(展示次数;触发清零)
|
||||
var ticket_sold := 0 # 专长凭证限购计数(选中/买下即 +1)
|
||||
var ticket_appeared := 0 # 专长凭证出现计数(quota 上限面)
|
||||
var _apply_pick := Callable() # 宿主兑付回调(AdventureRunner._apply_curio_pick)
|
||||
var _redeem_ticket := Callable() # 宿主凭证兑付回调(AdventureRunner._redeem_feat_ticket)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -21,12 +18,6 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
rng = rng_ref
|
||||
|
||||
|
||||
# 宿主回调注入(商店购买消费面经此回宿主兑付单点——core↔core 方法 Callable,禁 lambda 捕获 self)
|
||||
func bind_handlers(apply_pick: Callable, redeem_ticket: Callable) -> void:
|
||||
_apply_pick = apply_pick
|
||||
_redeem_ticket = redeem_ticket
|
||||
|
||||
|
||||
# 池与计数归零(宿主停点复位调用的池半;令牌池按表配额重建——池消耗即上限,账本随池重生)
|
||||
func reset() -> void:
|
||||
taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0}
|
||||
@@ -101,18 +92,6 @@ func consume_deck_token(kind: String, tier: String, feat_id: String = "") -> boo
|
||||
return false
|
||||
|
||||
|
||||
# 商店购买消费入口(LevelRunner.buy_pool_item 经 Callable 调用):专长凭证买下即开(零摘牌不进持有集);
|
||||
# 其余件同口回宿主兑付单点(消费在架令牌本体 kind+tier——写加算区+账本+摘牌与奖励三选一真单点)。
|
||||
# 未知 kind 拒绝(货架词表封闭的前置防御)。
|
||||
func consume_pool_purchase(kind: String, tier: String) -> bool:
|
||||
if kind == "feat_ticket":
|
||||
return bool(_redeem_ticket.call())
|
||||
if not db.adventure.CURIO_KINDS.has(kind):
|
||||
push_error("[冒险结构] 商店购买消费拒绝——件不在池词表: %s" % kind)
|
||||
return false
|
||||
return bool(_apply_pick.call(kind, tier, int(db.adventure.curio_value(kind, tier))))
|
||||
|
||||
|
||||
# 已拿上限判定(身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段;
|
||||
# taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。
|
||||
func cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
@@ -127,7 +106,7 @@ func cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
return int(taken.get(kind, 0)) < cap
|
||||
|
||||
|
||||
# 专长凭证展示判定(四口径:一局限购一次/罕见只在概率不在价格/计步保底/触发清零。
|
||||
# 专长凭证展示判定(四口径:一局限购一次/稀有只在概率不在价格/计步保底/触发清零。
|
||||
# 调用即一次展示判定与记账——展示次数计 pity、配额与限购双闸)
|
||||
func feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
|
||||
if ticket_sold >= int(db.adventure.feat_ticket_limit):
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
## 三选一停点件:停点编排唯一所有者(架构原则 9 本域承载)。
|
||||
## 停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途);刷新账三源(X 刷新件每碰三选一
|
||||
## 重给/优先消耗不结转、Y 营地升级行冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久——账面归
|
||||
## RefreshBank 件、编排留本件);兑付方法族(pick/refresh/redeem/skip);凭证出箱(RAP/时沙待注入
|
||||
## ——战首取走清零);三选一信号面自带。
|
||||
## 零自动推进停点语义承载:兑付只经 pick/apply 显式入口,停点在途时推进被拒(宿主关序受理读放行谓词)。
|
||||
## 生命周期=每冒险一次(AdventureRunner 冒险开始建;关卡换实例不换件——每关注入时点经 rebind_level_runner)。
|
||||
## 随机只经注入 CurioDeck 的 reward_pick 封闭流;兑付落账经注入主角账件(HeroStats/GainJournal)
|
||||
## 单写原语;落盘经注入方法 Callable(core↔core 接线禁 lambda 捕获 self)。Core 层零 UI。
|
||||
class_name CurioStop
|
||||
extends RefCounted
|
||||
|
||||
signal curio_choice_presented(payload: Dictionary) ## 专长三选一停点(非末关的关卡完成恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
|
||||
signal curio_resolved(result: Dictionary) ## 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
|
||||
|
||||
var db: ConfigDb
|
||||
var _deck: CurioDeck # 令牌池件(抽签/摘牌/cap/凭证判定——纯池)
|
||||
var _stats: HeroStats # 主角数值账(生命/特性——兑付回血与 relic 入集单写原语)
|
||||
var _journal: GainJournal # 加算日记件(兑付落账单写原语——加算区/钥石读面)
|
||||
var _snapshot_call := Callable() # 落盘回调(宿主断点快照受理——发射点后触发)
|
||||
var _grant_gp := Callable() # 当前关卡对象 grant_curio_gp 方法 Callable(GP 件入账维持现路径与 wallet_changed 发射序——每关注入时点更新,防调死对象)
|
||||
# ---- 停点三态 ----
|
||||
var _pending := {} # 关卡完成三选一在途:{"candidates": Array, "payload": Dictionary}——空 = 无停点
|
||||
var _resolved := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
|
||||
var _node_pending := {} # 奖励节点三选一停点态(共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
|
||||
# ---- 刷新账(三源账与付费计数归 RefreshBank 件——编排留本件:查停点态→bank 支出→重抽重发)----
|
||||
var _bank: RefreshBank # 刷新账件(构造注入——零停点态依赖)
|
||||
# ---- 凭证出箱(奇物.md 凭证件表——节点级/回合级一次性,战首取走清零)----
|
||||
var _pending_rap := 0 # 储备金袋待注入(下一战 RAP +N,战首注入后清零)
|
||||
var _time_sand_queue := 0 # 时沙待注入(下一战首轮 AP +N,战首入队后清零)
|
||||
var _level_index := 0 # 兑付载荷关卡序号(宿主每关注入——与 AdventureRunner.level_index 同源单写)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, deps: Dictionary, snapshot_call: Callable) -> void:
|
||||
db = db_ref
|
||||
_deck = deps.get("deck", null) as CurioDeck
|
||||
_stats = deps.get("stats", null) as HeroStats
|
||||
_journal = deps.get("journal", null) as GainJournal
|
||||
_bank = deps.get("bank", null) as RefreshBank
|
||||
_snapshot_call = snapshot_call
|
||||
|
||||
|
||||
# 每关注入时点(AdventureRunner._start_current_level 建件接线处调用):关卡序号快照+
|
||||
# 当前关卡对象 GP 入账方法 Callable(第二关起绑新 runner——防跨关调死对象)
|
||||
func rebind_level_runner(runner: LevelRunner, level_index: int) -> void:
|
||||
_level_index = level_index
|
||||
if runner != null:
|
||||
_grant_gp = runner.grant_curio_gp
|
||||
|
||||
|
||||
# ---- 五组入口之一:关卡完成开停点(宿主结算受理调用——回血先行、抽签、发信号、落盘)----
|
||||
|
||||
# 关卡完成兑付(非末关恰一次;三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
|
||||
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)。调用方(宿主关序)须先置
|
||||
# 「已兑付待推进」关序位(信号同步派发——处理器内立即确认时推进位必须已就位)。
|
||||
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)
|
||||
_stats.hero_hp = hp_before + heal
|
||||
_bank.reset_x() # 碰三选一重置 X 余量(不结转)
|
||||
_bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
var pick_payload: Dictionary[Variant, Variant] = {
|
||||
"level_index": _level_index,
|
||||
"candidates": _deck.draw_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=关卡完成渠道只出专长
|
||||
"heal_gained": heal,
|
||||
"hp_before": hp_before,
|
||||
"hp_after": _stats.hero_hp,
|
||||
"keystone_gained": int(_journal.keystone_drop_log().back().get("amount", 0)) if not _journal.keystone_drop_log().is_empty() else 0,
|
||||
"keystone_total": _journal.keystone_gained(),
|
||||
"gp": int(payload_base.get("gp", 0)), # 钱包读数由宿主随载荷_base 注入(停点件构造参只收 stats+journal——零钱包依赖)
|
||||
"aux_cleared": int(payload_base.get("aux_cleared", 0)), # additive 扩签(关卡末清空反馈取数)
|
||||
}
|
||||
_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
|
||||
"payload": pick_payload.duplicate(true)}
|
||||
curio_choice_presented.emit(pick_payload)
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
|
||||
|
||||
|
||||
# ---- 五组入口之二:奖励节点三选一 ----
|
||||
|
||||
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
|
||||
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
|
||||
func node_curio_candidates() -> Dictionary:
|
||||
var cands := _deck.draw_candidates("reward_node")
|
||||
return {"candidates": cands}
|
||||
|
||||
|
||||
func present_node_curio(candidates: Array) -> void:
|
||||
_bank.reset_x() # 碰三选一重置 X 余量(不结转)
|
||||
_bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
_node_pending = {"candidates": candidates.duplicate(true)}
|
||||
|
||||
|
||||
# ---- 五组入口之三:兑付族(选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
|
||||
|
||||
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件
|
||||
# 另须 feat_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
|
||||
# relic 件入特性持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束。
|
||||
# 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。
|
||||
func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||||
if _pending.is_empty():
|
||||
push_error("[停点件] pick_curio 拒绝——无待选停点")
|
||||
return false
|
||||
var candidates: Array[Variant] = _pending.get("candidates", []) as Array
|
||||
if _deck.find_token(candidates, kind, tier, feat_id).is_empty():
|
||||
push_error("[停点件] pick_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
|
||||
return false
|
||||
if kind == "feat_ticket":
|
||||
return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集)
|
||||
var value: int = int(db.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])
|
||||
return false
|
||||
_finish_curio_pick(kind, value, hp_before, tier, feat_id)
|
||||
return true
|
||||
|
||||
|
||||
# 兑付载荷 relic 伴随键(反馈行取数面;非 relic 件零新增键)
|
||||
func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||||
if feat_id == "":
|
||||
return {}
|
||||
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
|
||||
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
|
||||
|
||||
|
||||
# 三选一件兑付共享体(TP0——令牌消费、摘牌单点,含 relic 分支;领域拆分批拆三私有段:
|
||||
# 资源件段/路由加算段/副作用通道段,兑付语义与顺序逐字不变):
|
||||
# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限——
|
||||
# 与账本置满/关卡完成回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面
|
||||
# 按挂点消费,档位质变由 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():
|
||||
push_error("[停点件] relic 兑付失败——表无此件: %s" % feat_id)
|
||||
return false # 先校验后摘牌(令牌零丢失)
|
||||
if not _deck.consume_deck_token(kind, tier, feat_id):
|
||||
push_error("[停点件] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, feat_id])
|
||||
return false
|
||||
if _apply_resource_pick(kind, value, feat_id): # 资源件段(refresh/rap/time_sand/relic)
|
||||
return true
|
||||
if _apply_route_pick(kind, value): # 路由加算段(CURIO_APPLY_ROUTES 声明路由)
|
||||
return true
|
||||
return _apply_channel_pick(kind, value) # 副作用通道段(gp/heal 显式分支——未知件 false)
|
||||
|
||||
|
||||
# 资源件段(refresh/rap/time_sand/relic——冒险级资源与凭证账面;非资源件 false 交回主路由)
|
||||
func _apply_resource_pick(kind: String, value: int, feat_id: String) -> bool:
|
||||
if kind == "refresh":
|
||||
_bank.add_refresh_stock(value) # T4 F-1——刷新券入刷新存量账(冒险级;**不写 X 临时账**:X 每停点 reset_x 重置会吞存量;摘牌先于落账=选中即消尽)
|
||||
_bump_taken("refresh")
|
||||
return true
|
||||
if kind == "rap":
|
||||
_pending_rap += value # T4——储备金袋:下一战 RAP +N(节点级,战首注入后清零)
|
||||
_bump_taken("rap")
|
||||
return true
|
||||
if kind == "time_sand":
|
||||
_time_sand_queue += value # T4——时沙:下一战首轮 AP +N(回合级池增量,不改上限)
|
||||
_bump_taken("time_sand")
|
||||
return true
|
||||
if kind == "relic":
|
||||
if not _stats.feats.has(feat_id):
|
||||
_stats.feats.append(feat_id)
|
||||
_bump_taken("relic")
|
||||
_bump_taken("relic:" + feat_id) # 身份维已拿计数(cap 消费面)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# 路由加算段(match 收敛进效果类型:属性值提升四类走 adventure_table.CURIO_APPLY_ROUTES 声明
|
||||
# 路由(bonus 落点键),加新加算类令牌零代码)
|
||||
func _apply_route_pick(kind: String, value: int) -> bool:
|
||||
var sink := db.adventure.curio_apply_route(kind)
|
||||
if sink == "":
|
||||
return false
|
||||
_journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍)
|
||||
_bump_taken(kind)
|
||||
return true
|
||||
|
||||
|
||||
# 副作用通道段(副作用形态——显式分支:GP 件走关卡对象入账回调、恢复件当即回血)
|
||||
func _apply_channel_pick(kind: String, value: int) -> bool:
|
||||
match kind:
|
||||
"gp":
|
||||
if _grant_gp.is_valid():
|
||||
_grant_gp.call(value)
|
||||
_bump_taken("gp")
|
||||
"heal":
|
||||
_stats.hero_hp = mini(_stats.hero_hp + value, _stats.effective_max_hp())
|
||||
_bump_taken("heal")
|
||||
_:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 件获取次数计数递增(kind 维度派生账本——池件侧承载)
|
||||
func _bump_taken(kind: String) -> void:
|
||||
_deck.taken[kind] = int(_deck.taken.get(kind, 0)) + 1
|
||||
|
||||
|
||||
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
|
||||
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(反馈行取数)
|
||||
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void:
|
||||
_pending = {}
|
||||
_resolved = {
|
||||
"picked": picked,
|
||||
"tier": tier,
|
||||
"value": value,
|
||||
"hp_before": hp_before,
|
||||
"hp_after": _stats.hero_hp,
|
||||
"level_index": _level_index,
|
||||
}
|
||||
_resolved.merge(_feat_payload_keys(feat_id))
|
||||
curio_resolved.emit(_resolved.duplicate(true))
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call()
|
||||
|
||||
|
||||
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与关卡完成三选一同库;relic 同权入持有集)
|
||||
func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||||
if kind == "feat_ticket":
|
||||
return _redeem_feat_ticket() # T5 拿到即开——节点停点候选替换为专长三选一(呈现层本地重绘)
|
||||
if _node_pending.is_empty():
|
||||
push_error("[停点件] apply_node_curio 拒绝——无待选停点")
|
||||
return false
|
||||
var candidates: Array[Variant] = _node_pending.get("candidates", []) as Array
|
||||
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))
|
||||
if not _apply_curio_pick(kind, tier, value, feat_id):
|
||||
push_error("[停点件] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
|
||||
return false
|
||||
_finish_node_bonus(kind, value, tier, feat_id)
|
||||
return true
|
||||
|
||||
|
||||
# 停点空候选消化(池空=三选一环节整体跳过:候选 0 件时停点视同已处理,
|
||||
# 呈现层跳开选择面直接走后续推进。仅空候选合法;非空停点拒绝——零自动推进契约不破,正常
|
||||
# 消化仍只走 pick_curio/apply_node_curio 兑付。触发源=渠道白名单过滤后池空(关卡 Boss 击破=关卡完成
|
||||
# 渠道只出专长,专长零配置/旧档 deck 无 relic 余库即空集)。)
|
||||
func skip_empty_curio() -> bool:
|
||||
if _pending.is_empty():
|
||||
return false
|
||||
if not (_pending.get("candidates", []) as Array).is_empty():
|
||||
return false
|
||||
_pending = {}
|
||||
return true
|
||||
|
||||
|
||||
# 专长凭证兑付(拿到即开——停点候选替换为专长三选一;不进持有集/零摘牌;限购计数 +1):
|
||||
# 节点停点=本地替换(呈现层读在途态重绘);店内无停点=临时停点 + payload.context 标记(呈现层
|
||||
# 重 present 无胜利总结面,确认后只关面回店)。
|
||||
func _redeem_feat_ticket() -> bool:
|
||||
_deck.ticket_sold += 1 # 限购计数(本体在池件——原宿主转发属性迁点)
|
||||
_deck.ticket_pity = 0
|
||||
var cands := _deck.draw_candidates("level_clear") # 专长池(与关卡完成渠道同白名单)
|
||||
if cands.is_empty():
|
||||
push_error("[停点件] 专长凭证兑付失败——专长池空集")
|
||||
_deck.ticket_sold -= 1
|
||||
return false
|
||||
if not _node_pending.is_empty():
|
||||
_node_pending["candidates"] = cands.duplicate(true)
|
||||
return true
|
||||
var payload: Dictionary[Variant, Variant] = {"context": "feat_ticket", "candidates": cands.duplicate(true)}
|
||||
_pending = {"candidates": cands.duplicate(true), "payload": payload.duplicate(true)}
|
||||
curio_choice_presented.emit(payload)
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call()
|
||||
return true
|
||||
|
||||
|
||||
# ---- 五组入口之四:刷新编排(三源账与报价归 RefreshBank 件——本件只做停点态校验、
|
||||
# 支出分派与重抽重发;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/
|
||||
# 达上限剔牌/掷定前置直显;停点可中断)----
|
||||
|
||||
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
|
||||
func node_curio_pending_state() -> Dictionary:
|
||||
return {
|
||||
"candidates": (_node_pending.get("candidates", []) as Array).duplicate(true),
|
||||
}
|
||||
|
||||
|
||||
# 三选一刷新(免费位 X/Y——组合根唯一调用口;无停点/无余量 false 零副作用)。关卡完成停点重抽后重发
|
||||
# curio_choice_presented(呈现层重建卡片+刷新余量);节点停点由组合根读 node_curio_pending_state 重 present。
|
||||
func refresh_curio() -> bool:
|
||||
return _refresh_curio_common(false)
|
||||
|
||||
|
||||
# 三选一 GP 付费刷新(对齐商店 MC-1——免费位 X/Y 用尽后转付费,同旋钮同曲线同钱包;
|
||||
# 组合根先经 curio_stop_pending+钱包支出再调,本口只做计数与重抽)。池规则全保留——不放回/达上限剔牌/掷定前置直显。
|
||||
func refresh_curio_paid() -> bool:
|
||||
return _refresh_curio_common(true)
|
||||
|
||||
|
||||
# 三选一停点在途谓词(付费路径扣费前置校验——无停点禁扣钱,防空转烧钱包)
|
||||
func curio_stop_pending() -> bool:
|
||||
return not _pending.is_empty() or not _node_pending.is_empty()
|
||||
|
||||
|
||||
# 放行谓词(宿主关序受理——已兑付待推进且无在途停点)
|
||||
func stop_resolved() -> bool:
|
||||
return not _resolved.is_empty() and _pending.is_empty()
|
||||
|
||||
|
||||
func _refresh_curio_common(paid: bool) -> bool:
|
||||
if not curio_stop_pending():
|
||||
return false
|
||||
if paid:
|
||||
_bank.charge_paid() # 付费刷计数递增(报价曲线输入——账面归刷新账件)
|
||||
else:
|
||||
if not _bank.spend_free(): # 免费位消耗序 X→Y→存量(三源全空 false 零副作用——序与语义逐字归账件)
|
||||
return false
|
||||
if not _pending.is_empty():
|
||||
var cands := _deck.draw_candidates("level_clear")
|
||||
_pending["candidates"] = cands.duplicate(true)
|
||||
var payload: Dictionary[Variant, Variant] = (_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
payload["candidates"] = cands
|
||||
payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面
|
||||
curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call()
|
||||
return true
|
||||
if not _node_pending.is_empty():
|
||||
var cands2 := _deck.draw_candidates("reward_node")
|
||||
_node_pending["candidates"] = cands2.duplicate(true)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void:
|
||||
_node_pending = {}
|
||||
var payload := {
|
||||
"context": "reward_node",
|
||||
"picked": picked,
|
||||
"tier": tier,
|
||||
"value": value,
|
||||
"level_index": _level_index,
|
||||
}
|
||||
payload.merge(_feat_payload_keys(feat_id))
|
||||
curio_resolved.emit(payload)
|
||||
|
||||
|
||||
# 商店共享池消费单点(兑付单点——ShopSession.buy_pool_item 经注入 Callable 调用,回 _apply_curio_pick
|
||||
# 同口;专长凭证买下即开零摘牌)。未知 kind 拒绝(货架词表封闭的前置防御)。
|
||||
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):
|
||||
push_error("[停点件] 商店购买消费拒绝——件不在池词表: %s" % kind)
|
||||
return false
|
||||
return _apply_curio_pick(kind, tier, int(db.adventure.curio_value(kind, tier)))
|
||||
|
||||
|
||||
# ---- 冒险级复位(宿主 _reset_curio_state 调用——停点半:池归 deck.reset、停点三态与刷新账清零)----
|
||||
func reset_adventure_state() -> void:
|
||||
_pending = {}
|
||||
_resolved = {}
|
||||
_node_pending = {}
|
||||
_bank.reset_all() # 刷新三源与付费计数清零(账面归刷新账件)
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
|
||||
|
||||
# 续玩选择停点回填(token_migration 类:旧版断点候选为 kind 字符串,按普通档令牌防御归一;
|
||||
# 发射回宿主信号面由调用方承接——本件只载态)
|
||||
func restore_pending(bp_pending: Dictionary) -> void:
|
||||
var migrated: Array[Variant] = []
|
||||
for c in (bp_pending.get("candidates", []) as Array):
|
||||
if c is Dictionary:
|
||||
# T4——feat_id 随迁移(关卡完成渠道候选=纯专长令牌,身份键 relic|tier|feat_id 三段全要)
|
||||
var mig := {"kind": String((c as Dictionary).get("kind", "")), "tier": String((c as Dictionary).get("tier", ""))}
|
||||
if String(mig["kind"]) == "relic":
|
||||
mig["feat_id"] = String((c as Dictionary).get("feat_id", ""))
|
||||
migrated.append(mig)
|
||||
else:
|
||||
migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON})
|
||||
_pending = {"candidates": migrated}
|
||||
var payload: Dictionary[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
curio_choice_presented.emit(payload)
|
||||
|
||||
|
||||
# ---- 凭证出箱(战首取走清零——LevelRunner.set_voucher_grants 注入面同语义)----
|
||||
func voucher_outbox() -> Dictionary:
|
||||
var out := {"rap": _pending_rap, "time_sand": _time_sand_queue}
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
return out
|
||||
|
||||
|
||||
# 已兑付态清除(宿主 continue_to_next_level 推进受理时调用——重兑付防御复位面)
|
||||
func clear_resolved() -> void:
|
||||
_resolved = {}
|
||||
|
||||
|
||||
# 已兑付态注入+重发(宿主 resume 已兑付分支调用——置态后重发载荷,信号面归停点件:
|
||||
# 续玩不重兑付只重发;原 restore_resolved/reemit_resolved 两步收编一步,时序逐位同)
|
||||
func restore_resolved(payload: Dictionary) -> void:
|
||||
_resolved = payload.duplicate(true)
|
||||
curio_resolved.emit(_resolved.duplicate(true))
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件)----
|
||||
# 片段键:bonus_pending/bonus_result/feat_ticket_pity/feat_ticket_sold
|
||||
#(feat_ticket_* 现役为宿主字段——随停点件迁移同片段承载;refresh_stock/attune_refresh_y 两键
|
||||
# 归 RefreshBank 片段,宿主拼盘同一并集)
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"bonus_pending": _pending.duplicate(true),
|
||||
"bonus_result": _resolved.duplicate(true),
|
||||
"feat_ticket_pity": _deck.ticket_pity,
|
||||
"feat_ticket_sold": _deck.ticket_sold,
|
||||
}
|
||||
|
||||
|
||||
# 片段回填(凭证计数;停点在途态与已兑付态归宿主主分支
|
||||
# restore_pending/restore_resolved 承接——token_migration 与信号发射时序在彼处)
|
||||
func restore_fragment(frag: Dictionary, _params: Dictionary) -> void:
|
||||
_deck.ticket_pity = int(frag.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
|
||||
_deck.ticket_sold = int(frag.get("feat_ticket_sold", 0))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi31ynewc51ve
|
||||
@@ -1,6 +1,6 @@
|
||||
## EffectContext——效果引擎四桶计数器引用束(无状态引擎的外部状态承载)。
|
||||
## 四桶按生命周期分:round(轮始整桶清空)/battle(随编排器生灭)/level(随 LevelRunner 生灭)/
|
||||
## adventure(随 RunLedger 生灭)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
|
||||
## adventure(随冒险应用层生灭——桶归宿主字典)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
|
||||
class_name EffectContext
|
||||
extends RefCounted
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ func _init(db_ref: ConfigDb = null) -> void:
|
||||
## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备
|
||||
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器四桶由宿主持有、随宿主生灭,
|
||||
## 本方法只重建订阅集(运行时可变状态=零;battle/level 桶的清零由宿主 new 天然承载,round 桶清零
|
||||
## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随 RunLedger 引用跨战持久——无副本搬运)。
|
||||
## 在 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:
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
## 加算日记件:统一冒险加算区与获得记录唯一所有者。获得记录三层结构=获得记录列表
|
||||
## (_record_gain 追加、返回记录 id)→ 属性增益明细(带来源指回)→ 四轴汇总(由明细同步维护;
|
||||
## summary 与断点读汇总,日记为现行事实源)。三写通道统一走 gain_bonus 单写原语:三选一属性件
|
||||
## (source_class="curio_pick")/商店属性商品("shop_product"、kind=product_id)/战末导出
|
||||
## ("battle_export"、kind=轴名)。本局钥石增量与逐杀明细、真视使用记录、商店已购档数记账同域。
|
||||
## 生命与钱包各归 HeroStats/GpWallet——主角账三件各交各的断点片段,拼盘落盘归冒险应用层。
|
||||
## Core 层零 UI;随机零消费。
|
||||
class_name GainJournal
|
||||
extends RefCounted
|
||||
|
||||
# 加算区四轴(键序即断点 bonus 键序——与拆件前共享账置账形态逐位同形)
|
||||
const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"]
|
||||
|
||||
var _gain_log: Array[Variant] = [] # 获得记录列表(三选一件/商店购买/战末奖励各一条)
|
||||
var _bonus_detail: Array[Variant] = [] # 属性增益明细(带来源指回 gain_id)
|
||||
var _bonus_total: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
var _keystone := 0 # 钥石入账累计(摘要第三件事的账面)
|
||||
var _keystone_drops: Array[Variant] = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops)
|
||||
var _vision_used := 0 # 真视使用记录(本局计数——冒险收束聚合入档)
|
||||
var _purchased: Dictionary[Variant, Variant] = {} # product_id → 已购档数(递增定价输入;冒险开始清零)
|
||||
|
||||
|
||||
# ---- 获得记录三层 ----
|
||||
|
||||
# 获得记录列表追加(三通道各一条——source_class 词表:curio_pick/shop_product/battle_export;
|
||||
# 返回记录 id 供明细来源指回)
|
||||
func _record_gain(source_class: String, kind: String, detail: Dictionary) -> int:
|
||||
var rec_id := _gain_log.size() + 1 # 记录 id = 列表序派生(1 起——恢复面按 max(id) 校正)
|
||||
var rec: Dictionary[Variant, Variant] = {"id": rec_id, "source_class": source_class, "kind": kind}
|
||||
rec.merge(detail.duplicate(true))
|
||||
_gain_log.append(rec)
|
||||
return rec_id
|
||||
|
||||
|
||||
# 统一冒险加算区单写原语(三通道统一入口。明细+汇总同步维护——写时同拍,无事后对账)
|
||||
func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void:
|
||||
var gain_id := _record_gain(source_class, kind, {"axis": axis, "value": value})
|
||||
_bonus_detail.append({"gain_id": gain_id, "axis": axis, "value": value,
|
||||
"source_class": source_class, "kind": kind})
|
||||
_bonus_total[axis] = int(_bonus_total.get(axis, 0)) + value
|
||||
|
||||
|
||||
# 汇总读面(生效公式/断点/摘要唯一取数口)
|
||||
func bonus_total(axis: String) -> int:
|
||||
return int(_bonus_total.get(axis, 0))
|
||||
|
||||
|
||||
func bonus_snapshot() -> Dictionary[Variant, Variant]:
|
||||
return _bonus_total.duplicate(true)
|
||||
|
||||
|
||||
# 商店已购档数记账(购买条目——递增定价输入;定价读数走 purchase_tier)
|
||||
func record_purchase(product_id: String) -> void:
|
||||
_purchased[product_id] = purchase_tier(product_id) + 1
|
||||
|
||||
|
||||
func purchase_tier(product_id: String) -> int:
|
||||
return int(_purchased.get(product_id, 0))
|
||||
|
||||
|
||||
# 本局钥石增量(Boss 逐杀掉落渠道——首杀/复杀金额由调用方按存档标记判读后入行)
|
||||
func record_keystone_drop(drop: Dictionary) -> void:
|
||||
_keystone_drops.append(drop.duplicate(true))
|
||||
_keystone += int(drop.get("amount", 0))
|
||||
|
||||
|
||||
func keystone_gained() -> int:
|
||||
return _keystone
|
||||
|
||||
|
||||
func keystone_drop_log() -> Array:
|
||||
return _keystone_drops.duplicate(true)
|
||||
|
||||
|
||||
# 真视使用记录(真视类工具受理成功恰计一次;冒险收束聚合入档)
|
||||
func record_vision_use() -> void:
|
||||
_vision_used += 1
|
||||
|
||||
|
||||
func vision_used() -> int:
|
||||
return _vision_used
|
||||
|
||||
|
||||
# 日记域账面重置(冒险开始/单关重开共用值面——宿主须先调本件再调 HeroStats 置满,生命口径
|
||||
# 才不含加算区)
|
||||
func reset_adventure_accounts() -> void:
|
||||
_bonus_total = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
_gain_log = []
|
||||
_bonus_detail = []
|
||||
_purchased = {}
|
||||
_keystone = 0
|
||||
_keystone_drops = []
|
||||
_vision_used = 0
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)----
|
||||
# 片段键:bonus(汇总快照)/shop_purchased/keystone/keystone_drops/vision_counter_used
|
||||
# +获得日记键(唯一声明的断点扩列;旧档缺日记→汇总直载 clamp、在档→明细推导比对不符
|
||||
# push_error 拒载)
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"bonus": bonus_snapshot(),
|
||||
"shop_purchased": _purchased.duplicate(true),
|
||||
"keystone": _keystone,
|
||||
"keystone_drops": keystone_drop_log(),
|
||||
"vision_counter_used": _vision_used,
|
||||
"gain_journal": {"records": _gain_log.duplicate(true), "details": _bonus_detail.duplicate(true)},
|
||||
}
|
||||
|
||||
|
||||
# 片段回填(日记缺键 = 旧档——汇总直载跳过比对;日记在档 = 由明细推导汇总、与留存汇总不符
|
||||
# 即报错中止——日记为现行事实源,汇总仅作一致性校验读数)
|
||||
func restore_fragment(frag: Dictionary, _params: Dictionary) -> void:
|
||||
_purchased = (frag.get("shop_purchased", {}) as Dictionary).duplicate(true)
|
||||
_keystone = int(frag.get("keystone", 0))
|
||||
_keystone_drops = (frag.get("keystone_drops", []) as Array).duplicate(true)
|
||||
_vision_used = maxi(0, int(frag.get("vision_counter_used", 0))) # clamp 类(现役读侧策略同款)
|
||||
var journal: Variant = frag.get("gain_journal", null)
|
||||
if journal is Dictionary and not (journal as Dictionary).is_empty():
|
||||
_restore_from_journal(journal as Dictionary, frag)
|
||||
return
|
||||
# 旧档回退:汇总键直载(clamp 类——四轴子键夹 ≥0)
|
||||
_bonus_total = {}
|
||||
var bp_bonus: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary
|
||||
for axis in BONUS_AXES:
|
||||
_bonus_total[axis] = maxi(0, int(bp_bonus.get(axis, 0)))
|
||||
|
||||
|
||||
# 日记回填(记录列表与明细全量还原;汇总由明细推导——与留存汇总逐轴比对,不符即致命中止)
|
||||
func _restore_from_journal(journal: Dictionary, frag: Dictionary) -> void:
|
||||
_gain_log = (journal.get("records", []) as Array).duplicate(true)
|
||||
_bonus_detail = (journal.get("details", []) as Array).duplicate(true)
|
||||
var derived: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
for d in _bonus_detail:
|
||||
var axis := String((d as Dictionary).get("axis", ""))
|
||||
if not derived.has(axis):
|
||||
push_error("[加算日记] 断点日记明细含未知加算轴: %s(拒绝载入)" % axis)
|
||||
derived.clear()
|
||||
break
|
||||
derived[axis] = int(derived[axis]) + int((d as Dictionary).get("value", 0))
|
||||
if derived.is_empty():
|
||||
return # 已报错中止(fail-fast——明细非法整面拒绝)
|
||||
var stored: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary
|
||||
for axis in derived.keys():
|
||||
if int(stored.get(axis, -1)) != int(derived[axis]):
|
||||
push_error("[加算日记] 断点汇总与日记推导不一致(axis=%s 汇总=%d 推导=%d)——拒绝载入"
|
||||
% [axis, int(stored.get(axis, -1)), int(derived[axis])])
|
||||
return
|
||||
_bonus_total = derived
|
||||
@@ -0,0 +1 @@
|
||||
uid://chssg25j1e75a
|
||||
@@ -0,0 +1,36 @@
|
||||
## GP 钱包件:冒险局内货币唯一所有者(gain/spend/clear 单写原语;LevelRunner 与 ShopSession
|
||||
## 读写全指此单点)。生命与加算日记各归 HeroStats/GainJournal——主角账三件各交各的断点片段,
|
||||
## 拼盘落盘归冒险应用层。Core 层零 UI;随机零消费。
|
||||
class_name GpWallet
|
||||
extends RefCounted
|
||||
|
||||
var gp: int = 0 # GP 钱包(冒险开始 = 0、收束清零;不入存档不入摘要)
|
||||
|
||||
|
||||
func gain_gp(amount: int) -> void:
|
||||
gp = maxi(0, gp + amount)
|
||||
|
||||
|
||||
# 余额不足 false 零副作用(出账前置校验由调用方守卫时本方法不重复拒绝)
|
||||
func spend_gp(amount: int) -> bool:
|
||||
if amount <= 0 or gp < amount:
|
||||
return false
|
||||
gp = gp - amount
|
||||
return true
|
||||
|
||||
|
||||
func clear_wallet() -> void:
|
||||
gp = 0
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)----
|
||||
# 片段键:gp
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"gp": gp,
|
||||
}
|
||||
|
||||
|
||||
func restore_fragment(frag: Dictionary, _params: Dictionary) -> void:
|
||||
gp = int(frag.get("gp", 0))
|
||||
@@ -0,0 +1 @@
|
||||
uid://byi84vj4wyg1y
|
||||
@@ -0,0 +1,118 @@
|
||||
## 主角数值账:冒险生命周期主角数值域唯一所有者(生命账与特性持有集、锻造强化快照与调试浮窗
|
||||
## 百分比通道、攻击/甲类型轴快照、生效上限/攻击/护甲公式与回血封顶公式单点、续玩生命钳制收口)。
|
||||
## 生效公式消费加算日记域汇总(注入 GainJournal 只读引用——无反向引用不成环);GP 钱包归
|
||||
## GpWallet、加算日记归 GainJournal——主角账三件各交各的断点片段,拼盘落盘归冒险应用层。
|
||||
## Core 层零 UI;随机零消费。
|
||||
class_name HeroStats
|
||||
extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var journal: GainJournal # 加算日记只读引用(生效公式的加算区取数口——构造注入,宿主同源单件)
|
||||
var hero_hp: int = 0 # 生命账
|
||||
var feats: Array[String] = [] # 特性持有集(活引用注入——关内兑付 relic 须当关生效;收束清零)
|
||||
var attack_type := "" # 攻击类型轴快照(空串=未注入→锻造配置默认值回退)
|
||||
var armor_type := "" # 甲类型轴快照(同上)
|
||||
var _base_override: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
var _override: Array[int] = [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 应用面)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, base_override_ref: Dictionary, journal_ref: GainJournal) -> void:
|
||||
db = db_ref
|
||||
journal = journal_ref
|
||||
_base_override = _normalize_override(base_override_ref)
|
||||
|
||||
|
||||
# 永久强化 flat 重注(单关测试模式 start_level 收 base_override_in 时调用——实例不重建,值重注)
|
||||
func rebase_override(raw: Dictionary) -> void:
|
||||
_base_override = _normalize_override(raw)
|
||||
|
||||
|
||||
# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||
func set_debug_override(atk_pct: int, hp_pct: int) -> void:
|
||||
_override = [atk_pct, hp_pct]
|
||||
|
||||
|
||||
# 续玩生命钳制(断点可能带出高于生效上限的账本值——恢复后钳到保守上限:表值+永久强化 flat
|
||||
# 后乘调试 pct、下限 1;口径不含加算区,加算区恢复后其余战斗照常。原宿主 _resume_params 的
|
||||
# mini 式逐字收进)
|
||||
func clamp_hp_to_resume_cap(db_max_hp: int) -> void:
|
||||
hero_hp = mini(hero_hp, maxi(1,
|
||||
int(round(float(int(db_max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0))))
|
||||
|
||||
|
||||
# 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态)
|
||||
func _normalize_override(raw: Dictionary) -> Dictionary[Variant, Variant]:
|
||||
var out: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
for k in out.keys():
|
||||
out[k] = maxi(0, int(raw.get(k, 0)))
|
||||
return out
|
||||
|
||||
|
||||
# ---- 生效公式(单点——叠加次序钉死:表值 → 永久强化 flat → 加算区 flat 先加,
|
||||
# 调试浮窗百分比后乘;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")
|
||||
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")
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[0])) / 100.0)))
|
||||
|
||||
|
||||
# 护甲等级生效值(入战 AC 三层可加:甲型基础 AC + 甲路线等级加值 + 加算区 armor_class 轴;
|
||||
# 无调试浮窗百分比通道,纯 flat)
|
||||
func effective_ac() -> int:
|
||||
return maxi(0, _armor_base_ac() + int(_base_override.get("armor_class", 0)) + journal.bonus_total("armor_class"))
|
||||
|
||||
|
||||
# 甲型基础 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)
|
||||
if row.is_empty():
|
||||
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
|
||||
return int(row.get("base_ac", 0))
|
||||
|
||||
|
||||
# 回血公式单点(恢复点节点兑付/账本置满封顶同式;算术宿主——原 LevelRunner.rest_heal 迁入)
|
||||
static func rest_heal(pct: int, hp: int, max_hp: int) -> int:
|
||||
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
|
||||
|
||||
|
||||
# 单关模式重开重置(值重置非实例重建——shop 绑定引用不换代;语义=原单关 start_level 重置面。
|
||||
# 钱包/日记清零各归各件,由宿主在本调用前完成——日记先清再置满,生命口径才不含加算区)
|
||||
func reset_for_single_level() -> void:
|
||||
_base_override = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
reset_adventure_accounts()
|
||||
|
||||
|
||||
# 冒险账面重置(冒险开始/单关重开共用值面——生命置满基础上限、特性持有集清零)
|
||||
func reset_adventure_accounts() -> void:
|
||||
feats = []
|
||||
hero_hp = effective_max_hp()
|
||||
|
||||
|
||||
# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写)
|
||||
func feats_held() -> Array:
|
||||
return feats.duplicate()
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)----
|
||||
# 片段键:hero_hp/relics(其余账面键归 GpWallet/GainJournal 各自片段)
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"hero_hp": hero_hp,
|
||||
"relics": feats.duplicate(),
|
||||
}
|
||||
|
||||
|
||||
# 片段回填(生命直载;特性持有集逐项归一 String——旧档防御同款)
|
||||
func restore_fragment(frag: Dictionary, _params: Dictionary) -> void:
|
||||
hero_hp = int(frag.get("hero_hp", 0))
|
||||
feats = []
|
||||
for r in (frag.get("relics", []) as Array):
|
||||
feats.append(String(r))
|
||||
@@ -0,0 +1 @@
|
||||
uid://boeklf03gti5b
|
||||
+120
-149
@@ -2,8 +2,10 @@
|
||||
## 关卡域生命周期编排者:节点图行进、HP 跨战斗携带、恢复点兑付、关卡末收束(垂直切片验证:一关内数值提升可感知);商店族归 ShopSession 件。
|
||||
## Core 层零 UI 依赖(headless 可跑完整关卡);零自动推进——兑付确认后经 choices_presented,推进只走 pick_next_node。
|
||||
## 种子域:单关模式 start_level 自播种;冒险模式 start_level_external 不重播种(主种子恰一次归 AdventureRunner)。
|
||||
## hero_atk_pct/hero_hp_pct 为测试专用覆盖——经 encounter 显式传入战斗,零表写,db.hero 运行期只读。
|
||||
## 调试浮窗百分比通道单点归主角账(set_hero_override 转调——公式消费面同源)。
|
||||
## 信号顺序契约:呈现层须在 level_started 回调重取 runner 实例,choices_presented 才不会被空防御丢弃。
|
||||
## 关卡开始信号前零发射:本件全部信号在 level_started 发射前保持静默(构造与 start 收参零信号——
|
||||
## defer_entry 两步式以 level_started 为界,先广播后呈现入口候选;配断言面见冒烟源码断言)。
|
||||
class_name LevelRunner
|
||||
extends RefCounted
|
||||
|
||||
@@ -22,18 +24,23 @@ var node_index := -1
|
||||
# ---- 账本载体(冒险结构实现批——漂移②清偿)----
|
||||
# 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本);
|
||||
# 单关模式 = 本实例私有账本(start_level 重建,既有单关语义逐位保留)。
|
||||
var _ledger: Dictionary[Variant, Variant] = {"hero_hp": 0, "gp": 0}
|
||||
var level_counters: Dictionary[Variant, Variant] = {} # 关卡桶(lvl_ 前缀计数器;每关随 LevelRunner new 天然空=按关清零,替代旧硬编码清除名单)
|
||||
var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③)
|
||||
var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点)
|
||||
# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落 _ledger)
|
||||
# 主角账三件引用(领域拆分批三件化:冒险模式经 ctx 注入共享三件、单关模式 _init 自建私有件;
|
||||
# 数值账消费日记汇总——私有默认件同源成对建)
|
||||
var _journal: GainJournal
|
||||
var _wallet: GpWallet
|
||||
var _stats: HeroStats
|
||||
var loadout = null # 同调配装件(构造注入;直构=单关零件)
|
||||
var _adv_bucket: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——冒险模式随 ctx 注入;单关=本实例自有)
|
||||
# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落主角账三件)
|
||||
var hero_hp: int:
|
||||
get: return int(_ledger["hero_hp"])
|
||||
set(v): _ledger["hero_hp"] = v
|
||||
get: return _stats.hero_hp
|
||||
set(v): _stats.hero_hp = v
|
||||
var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要)
|
||||
get: return int(_ledger["gp"])
|
||||
set(v): _ledger["gp"] = v
|
||||
var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零)
|
||||
get: return _wallet.gp
|
||||
set(v): _wallet.gp = v
|
||||
var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主)
|
||||
# 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口
|
||||
var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null)
|
||||
@@ -45,28 +52,6 @@ var _attunement: Array[Variant] = [] # 同调装配集(adve
|
||||
# 消费面:effective_ac 甲型基础 AC 层 + begin_battle enc 携带进 unit_container.spawn 英雄快照)
|
||||
var _hero_attack_type := ""
|
||||
var _hero_armor_type := ""
|
||||
# ---- 商店共享池模式态(缺省 legacy=固定三件加定价 off 对照承载,零 RNG 零池消费;TP0 令牌池)----
|
||||
# 件内状态白盒读面(断点写侧与既有取数面沿用原字段名;状态本体单点在 ShopSession)
|
||||
var _shop_pool_mode: bool:
|
||||
get: return _shop.pool_mode
|
||||
var _pool_deck: Array[Variant]:
|
||||
get: return _shop.pool_deck
|
||||
var _pool_consume: Callable:
|
||||
get: return _shop.pool_consume
|
||||
var _pool_ticket: Callable:
|
||||
get: return _shop.pool_ticket
|
||||
var _shop_shelf: Array[Variant]:
|
||||
get: return _shop.shelf
|
||||
var _shop_visit_buys: int:
|
||||
get: return _shop.visit_buys
|
||||
var _shop_paid_refreshes: int:
|
||||
get: return _shop.paid_refreshes
|
||||
var _shop_purchased: Dictionary[Variant, Variant]: # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||
get: return _shop.purchased
|
||||
set(v): _shop.purchased = v
|
||||
|
||||
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
|
||||
var hero_hp_pct := 0
|
||||
# ---- 关卡相位轴(显式 Phase enum:内部 enum + 守卫换算,对齐 行动经济 Phase 先例;
|
||||
# level_result:String 与 awaiting_choice() 是 AdventureRunner/呈现层消费的外部契约面)----
|
||||
enum Phase { IDLE, RUNNING, AWAITING_CHOICE, ENDED }
|
||||
@@ -77,10 +62,45 @@ var visited: Array[Dictionary] = [] # 已兑付/到达节点
|
||||
var _generator # 参数化长图生成器(惰性建——NodeMapGenerator 持注入的 RngService)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
# 构造注入一次给齐(后置 setter 群与商店重绑机制退役;_bind_shop 全仓仅本处构造调用)。
|
||||
# ctx 键(冒险模式由 AdventureRunner._start_current_level 组包;缺省空字典 = 单关测试模式:
|
||||
# 私有主角账三件自建〔空 override、数值账同源消费私有日记〕、冒险桶本实例自有、pool 模式零注入):
|
||||
# stats:HeroStats/wallet:GpWallet/journal:GainJournal(共享主角账三件——活引用)/loadout:AttuneLoadout(同调配装件)/adv_bucket:Dictionary(冒险桶宿主字典)
|
||||
# attunement:Array(同调装配集——活引用)/tree_rules:Array(树 rule 键集——活引用)/shop_mode:String("pool" 才注入池面)
|
||||
# treasure_unlocked:Dictionary/hero_attack_type:String/hero_armor_type:String
|
||||
# voucher_rap:int/voucher_time_sand:int(凭证出箱注入面)
|
||||
# pool_wiring:Dictionary(池模式装配 4 参:{deck:Array, consume:Callable, ticket:Callable, cap:Callable}——缺省空字典 = legacy 零注入)
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
if ctx.get("stats", null) is HeroStats:
|
||||
_stats = ctx["stats"]
|
||||
_wallet = ctx["wallet"] if ctx.get("wallet", null) is GpWallet else GpWallet.new()
|
||||
_journal = ctx["journal"] if ctx.get("journal", null) is GainJournal else GainJournal.new()
|
||||
_feats = _stats.feats # 特性集活引用注入(关内兑付 relic 当关生效——契约 6)
|
||||
else:
|
||||
_journal = GainJournal.new() # 直构私有三件(单关模式——实例不重建,值重置走 start_level)
|
||||
_wallet = GpWallet.new()
|
||||
_stats = HeroStats.new(null, {}, _journal)
|
||||
_stats.db = db # 直构私有账补表引用(单关模式——实例不重建,值重置走 start_level)
|
||||
var loadout_ref: Variant = ctx.get("loadout", null)
|
||||
loadout = loadout_ref if loadout_ref is AttuneLoadout else null # 直构=单关零 loadout(充能面跳过)
|
||||
if ctx.get("adv_bucket", null) is Dictionary:
|
||||
_adv_bucket = ctx["adv_bucket"]
|
||||
if ctx.get("treasure_inventory", null) is TreasureItems:
|
||||
treasure_inventory = ctx["treasure_inventory"] # 冒险级库存共享(同调未用行跨关存活——单源化承载面)
|
||||
_treasure_unlocked = ctx.get("treasure_unlocked", {}) if ctx.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = ctx.get("attunement", []) if ctx.get("attunement", []) is Array else [] # 活引用注入(契约 6——禁快照)
|
||||
_tree_rules = ctx.get("tree_rules", []) if ctx.get("tree_rules", []) is Array else [] # 活引用注入
|
||||
_hero_attack_type = String(ctx.get("hero_attack_type", ""))
|
||||
_hero_armor_type = String(ctx.get("hero_armor_type", ""))
|
||||
_voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0)))
|
||||
_voucher_time_sand = maxi(0, int(ctx.get("voucher_time_sand", 0)))
|
||||
_shop = ShopSession.new(db, rng)
|
||||
var pool: Dictionary[Variant, Variant] = ctx.get("pool_wiring", {}) as Dictionary
|
||||
if String(ctx.get("shop_mode", "")) == "pool" and not pool.is_empty():
|
||||
_shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable,
|
||||
pool.get("ticket", Callable()) as Callable, pool.get("cap", Callable()) as Callable)
|
||||
_bind_shop()
|
||||
|
||||
|
||||
@@ -92,49 +112,31 @@ func _is_running() -> bool:
|
||||
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)----
|
||||
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
hero_atk_pct = atk_pct
|
||||
hero_hp_pct = hp_pct
|
||||
_stats.set_debug_override(atk_pct, hp_pct)
|
||||
|
||||
|
||||
# 叠加次序(钉死):表值 → 永久强化 flat(base_override)→ 加算区 flat(bonus)先加
|
||||
# → 调试浮窗百分比后乘(调试数值工具恒乘最后)
|
||||
# 生效公式单点归数值账(本类删公式本体,读面转调;叠加次序注释随公式迁数值账)
|
||||
func effective_atk() -> int:
|
||||
var base: int = int(db.hero.atk) + int(base_override.get("atk", 0)) + int(run_ledger.bonus.get("atk", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_atk_pct)) / 100.0)))
|
||||
return _stats.effective_atk()
|
||||
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(base_override.get("max_hp", 0)) + int(run_ledger.bonus.get("max_hp", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0)))
|
||||
return _stats.effective_max_hp()
|
||||
|
||||
|
||||
# 护甲等级生效值(入战 AC 三层可加:甲型基础 AC〔forge.tres 选中甲路线 base_ac〕+
|
||||
# 甲路线等级加值〔base_override.armor_class——hero_override 锻造分量〕+ 加算区 armor_class 轴;
|
||||
# 无调试浮窗百分比通道,纯 flat)
|
||||
func effective_ac() -> int:
|
||||
return maxi(0, _armor_base_ac() + int(base_override.get("armor_class", 0)) + int(run_ledger.bonus.get("armor_class", 0)))
|
||||
return _stats.effective_ac()
|
||||
|
||||
|
||||
# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线=轻甲 10,
|
||||
# 与 hero.tres 两轴删除前 armor_class=10 行为连续)
|
||||
func _armor_base_ac() -> int:
|
||||
var at: String = _hero_armor_type if _hero_armor_type != "" else db.forge_default_armor_type()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at)
|
||||
if row.is_empty():
|
||||
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
|
||||
return int(row.get("base_ac", 0))
|
||||
|
||||
|
||||
# 行动点加成生效值(统一冒险加算区第四轴 ap;经战斗开始入参 hero_ap_bonus
|
||||
# 行动点加成生效值(统一冒险加算区第四轴;经战斗开始入参 hero_ap_bonus
|
||||
# 传入战斗域,兑现 = 每场战斗开始首轮行动点基线 +加成值〔行动经济 GDD 跨冒险行动点加成轴节〕)
|
||||
func effective_ap_bonus() -> int:
|
||||
return maxi(0, int(run_ledger.bonus.get("ap", 0)))
|
||||
return maxi(0, _journal.bonus_total("ap"))
|
||||
|
||||
|
||||
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
||||
func restore_hero_base() -> void:
|
||||
hero_atk_pct = 0
|
||||
hero_hp_pct = 0
|
||||
_stats.set_debug_override(0, 0)
|
||||
|
||||
|
||||
# ---- 关卡生命周期 ----
|
||||
@@ -148,7 +150,6 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
# 注入面只服务数值验证工具,非玩家可达路径)
|
||||
_external = false
|
||||
_rest_handler = rest_heal_amount
|
||||
_ledger = {"hero_hp": 0, "gp": 0}
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
@@ -158,14 +159,10 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
_phase = Phase.IDLE
|
||||
_candidates = []
|
||||
visited = []
|
||||
gp = 0 # 冒险钱包清零开局(局内货币生命周期起点)
|
||||
run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0(单关入口零元进度——注入面见上)
|
||||
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat:缺省全零白板(单关入口零元进度——注入面见上)
|
||||
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
|
||||
_bind_shop() # 钱包引用换代重绑(_ledger 已重建)
|
||||
for k in base_override.keys():
|
||||
base_override[k] = maxi(0, int(base_override_in.get(k, 0))) # 注入规范化(契约同冒险开始参数面)
|
||||
hero_hp = effective_max_hp()
|
||||
_stats.rebase_override(base_override_in) # 永久强化 flat 重注(规范化归数值账——契约同冒险开始参数面)
|
||||
_journal.reset_adventure_accounts() # 日记域账面清零(先于置满——生命口径不含加算区)
|
||||
_wallet.clear_wallet() # 钱包清零
|
||||
_stats.reset_for_single_level() # 数值账面重置(override 清零+生命置满+特性清零——实例不重建,shop 绑定不换代)
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
|
||||
if not _assign_generated_graph(row, seed_val):
|
||||
_phase = Phase.ENDED
|
||||
@@ -179,30 +176,24 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
return _enter_node(entries[0])
|
||||
|
||||
|
||||
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
|
||||
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
|
||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)。
|
||||
# base_override = 永久强化 flat 共享引用(缺省 {} 全零白板,与 B1 空参数路径同值)。
|
||||
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经主角账共享交接;
|
||||
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/加算区/已购档,
|
||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在主角账写好)。
|
||||
# 流程与 start_level 同款(_find_level_row → 生成图装配 → 状态初始化 → _enter_node(入口)),
|
||||
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
|
||||
# 差异仅在恢复执行体的注入与零重置(主角账三件/冒险桶经 ctx 构造注入)。
|
||||
# defer_entry 尾参(缺省 false = 直接进节点)——true 时不自动
|
||||
# _enter_node(entry),改置选路态(awaiting_choice + 单候选 = 入口节点),由 AdventureRunner 在
|
||||
# level_started 之后调 present_entry_choice() 呈现、玩家 pick_next_node 显式进首战。拆两步的
|
||||
# 信号顺序契约:呈现层须先经 level_started 重取 runner 实例,choices_presented 才不会被
|
||||
# _on_choices_presented 的 runner 空防御静默丢弃。入口呈现零 RNG 消耗、pick 走同一个
|
||||
# _enter_node(entry)——测量面零漂移红线。
|
||||
func start_level_external(level_index: int, ledger: Dictionary,
|
||||
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {},
|
||||
func start_level_external(level_index: int, rest_handler: Callable,
|
||||
defer_entry: bool = false, resume_state: Dictionary = {}) -> bool:
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
return false
|
||||
_external = true
|
||||
_ledger = ledger
|
||||
base_override = base_override_ref
|
||||
_shop_purchased = purchased_ref
|
||||
_bind_shop() # 钱包共享引用换代重绑(_ledger = 冒险账本)
|
||||
_rest_handler = rest_handler
|
||||
if not _assign_generated_graph(row, rng.master_seed):
|
||||
return false
|
||||
@@ -299,6 +290,33 @@ func restore_treasure_items(items: Array) -> void:
|
||||
treasure_inventory.restore_inventory(items)
|
||||
|
||||
|
||||
# ---- 断点自交片段(关卡域——拆件批 T4:应用层唯一拼盘落盘口的收件半边)----
|
||||
|
||||
# 关卡域片段(node_id/visited_ids/aux_items/shop_shelf/shop_visit_buys——五键自交;
|
||||
# 停点续玩分支 _runner 为 null 时节点域按缺省落档〔与真机同构〕)
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
var treasure_snapshot: Array[Variant] = []
|
||||
if treasure_inventory != null:
|
||||
treasure_snapshot = treasure_inventory.snapshot_inventory()
|
||||
return {
|
||||
"node_id": current_node_id(),
|
||||
"visited_ids": visited_ids(),
|
||||
"aux_items": treasure_snapshot,
|
||||
"shop_shelf": _shop.shop_bp_shelf(),
|
||||
"shop_visit_buys": _shop.shop_bp_visit_buys(),
|
||||
}
|
||||
|
||||
|
||||
# 断点重建参数面收口(原 _resume_bp 直读面收编——resume_state = {"node_id": int(-1 = 入口停点),
|
||||
# "visited_ids": Array[int], "aux_items": Array};空字典 = 既有路径逐位不变)。
|
||||
# 返回 resume_state 形态供 start_level_external 分派(_restore_to_stop / restore_treasure_items)。
|
||||
func restore_fragment(resume_state: Dictionary) -> Dictionary:
|
||||
var aux: Array[Variant] = resume_state.get("aux_items", []) as Array
|
||||
if not aux.is_empty():
|
||||
restore_treasure_items(aux)
|
||||
return resume_state
|
||||
|
||||
|
||||
func _find_level_row(level_index: int) -> Dictionary:
|
||||
return db.level_row(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器)
|
||||
|
||||
@@ -485,7 +503,7 @@ func _enter_node(node: Dictionary) -> bool:
|
||||
"shop":
|
||||
# 商店房兑付:到达即开店(覆盖层归呈现层),购买出账/
|
||||
# 离开归玩家意图——零自动推进;离开确认经 continue_after_node 回选路口
|
||||
if _shop_pool_mode:
|
||||
if _shop.pool_mode:
|
||||
_shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签)
|
||||
node_entered.emit(node, {"gp": gp, "shelf": true})
|
||||
return true
|
||||
@@ -516,7 +534,9 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
|
||||
"attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退)
|
||||
"attunement": _attunement,
|
||||
"ledger": run_ledger, # 共享四账单类下传(单引用键)
|
||||
"loadout": loadout, # 同调配装件下传(充能单源化——reset_cycle/consume_charge 消费面;直构 null=零装配)
|
||||
"journal": _journal, # 加算日记下传(战末 bonus 汇入/真视计数单写原语消费面)
|
||||
"adv_bucket": _adv_bucket, # 冒险桶下传(效果引擎 adv_ 计数器宿主字典——跨战持久同一引用)
|
||||
"level_counters": level_counters, # 关卡桶下传(同关跨战共享引用、随本类 new 天然空)
|
||||
"feats": _feats,
|
||||
"tree_rules": _tree_rules,
|
||||
@@ -572,7 +592,13 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
var treasure_cleared := 0
|
||||
if treasure_inventory != null:
|
||||
treasure_cleared = treasure_inventory.inventory_count() # 快照计数(清空前——「剩余道具随关卡结束清空」可见反馈取数)
|
||||
treasure_inventory.reset() # 关卡末库存清空(无论输赢,幂等)
|
||||
# 关卡末库存清空(无论输赢,幂等)——单源化承载面:同调未用行跨关存活(充能唯一事实源,
|
||||
# 原字典轨跨关记忆的行化等价——已耗行作废不保留=C 区投影与剩余读数同构)
|
||||
var keep: Array[Variant] = []
|
||||
for r in treasure_inventory.snapshot_inventory():
|
||||
if String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)):
|
||||
keep.append(r)
|
||||
treasure_inventory.restore_inventory(keep)
|
||||
level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index,
|
||||
"node_id": _node_id(node), "node_type": String(node.get("type", "")),
|
||||
"aux_cleared": treasure_cleared}) # additive 扩签(不参与派生——node_map 扩签先例)
|
||||
@@ -633,27 +659,10 @@ func pick_next_node(node_id: int) -> bool:
|
||||
func current_battle() -> Variant:
|
||||
return _settlement
|
||||
|
||||
# aux 解锁门控集注入(AdventureRunner 自 params 传递)
|
||||
func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
_treasure_unlocked = ids
|
||||
if _settlement != null:
|
||||
_settlement.set_treasure_unlocked(ids)
|
||||
|
||||
|
||||
# 同调装配集注入(冒险开始参数面;AdventureRunner 唯一调用方)
|
||||
func set_attunement(ids: Array) -> void:
|
||||
_attunement = ids.duplicate()
|
||||
_bind_shop() # 装配集共享引用换代重绑(刷新执照读数面)
|
||||
|
||||
|
||||
# 特性持有集注入(AdventureRunner._start_current_level 唯一调用方;begin_battle
|
||||
# enc.feats 携带进战斗求值;**共享引用不复制**——关内奖励节点兑付的 relic 须当关后续战斗生效
|
||||
# (attune_charges 共享账本同款);空集=零扰动)
|
||||
# 特性持有集(begin_battle enc.feats 求值面;**共享引用不复制**——关内奖励节点兑付的 relic 须当关生效)
|
||||
var _feats: Array[Variant] = []
|
||||
|
||||
|
||||
func set_feats(ids: Array) -> void:
|
||||
_feats = ids
|
||||
# 树 rule 键集(begin_battle enc.tree_rules 携带;Fork 激活侧 effects——构造注入活引用)
|
||||
var _tree_rules: Array[Variant] = []
|
||||
|
||||
|
||||
# 特性持有集读面(HUD C 区特性段数据源)
|
||||
@@ -661,59 +670,21 @@ func feats_held() -> Array:
|
||||
return _feats.duplicate()
|
||||
|
||||
|
||||
# 树 rule 键集注入(begin_battle enc.tree_rules 携带;Fork 激活侧 effects)
|
||||
var _tree_rules: Array[Variant] = []
|
||||
# 主角数值账只读出口(组件交出—— AdventureRunner.stats() 同款;v1.3 hero() 换代)
|
||||
func stats() -> HeroStats:
|
||||
return _stats
|
||||
|
||||
|
||||
func set_tree_rules(rules: Array) -> void:
|
||||
_tree_rules = rules.duplicate()
|
||||
_bind_shop() # rule 键集共享引用换代重绑(散折价判定面)
|
||||
# 加算日记只读出口(bonus 生产读面承接——呈现层选路屏/结算行经此读加算区)
|
||||
func journal() -> GainJournal:
|
||||
return _journal
|
||||
|
||||
|
||||
# 两根类型轴注入(adventure 参数面快照;空串=未注入→锻造配置默认值回退。
|
||||
# 消费面:effective_ac 甲型基础 AC 层 + begin_battle enc 携带进 unit_container.spawn 英雄快照)
|
||||
func set_hero_axes(attack_type: String, armor_type: String) -> void:
|
||||
_hero_attack_type = attack_type
|
||||
_hero_armor_type = armor_type
|
||||
|
||||
|
||||
# 共享四本账注入(RunLedger 单类下传,含 bonus/attune_charges/
|
||||
# vision_counter/adventure_counters 四账;战斗侧只走账类原语,战末 export/开局 seed
|
||||
# 时点语义不变)
|
||||
var run_ledger: RunLedger = RunLedger.new()
|
||||
|
||||
|
||||
func set_run_ledger(l: RunLedger) -> void:
|
||||
run_ledger = l
|
||||
_bind_shop() # 账类换代重绑(加算区 flat 落点面)
|
||||
|
||||
|
||||
# 凭证件三账注入(储备金袋 RAP/时沙 首轮 AP/完美保险 标错豁免;节点级一次性,
|
||||
# battle begin 消费后归零;缺省 0=零扰动)
|
||||
# 凭证出箱注入面(储备金袋 RAP/时沙 首轮 AP;节点级一次性,battle begin 消费后归零;缺省 0=零扰动)
|
||||
var _voucher_rap := 0
|
||||
var _voucher_time_sand := 0
|
||||
|
||||
|
||||
func set_voucher_grants(rap: int, time_sand: int) -> void:
|
||||
_voucher_rap = maxi(0, rap)
|
||||
_voucher_time_sand = maxi(0, time_sand)
|
||||
|
||||
|
||||
# 同调主动件充能账骑库存面注入(账本住
|
||||
# RunLedger;差额注入防续玩双份——resume 路径 restore_inventory
|
||||
# 已含断点内 attune 记录)
|
||||
func sync_attune_inventory() -> void:
|
||||
var charges: Dictionary[Variant, Variant] = run_ledger.attune_charges
|
||||
if treasure_inventory == null:
|
||||
treasure_inventory = TreasureItems.new(db)
|
||||
for iid in charges:
|
||||
var existing := 0
|
||||
for r in treasure_inventory.snapshot_inventory():
|
||||
if String(r.item_id) == String(iid) and String(r.source_class) == "attunement" and not bool(r.used):
|
||||
existing += 1
|
||||
for i in range(maxi(0, int(charges[iid]) - existing)):
|
||||
treasure_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0)
|
||||
|
||||
# 三选一 GP 袋件入账(钱包收口的「奖励入账」入口;复用战斗完成同一入账原语,
|
||||
# wallet_changed 载荷带 source 供呈现归因)
|
||||
func grant_curio_gp(amount: int) -> void:
|
||||
@@ -753,11 +724,11 @@ func close_wallet() -> void:
|
||||
# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);本类保留同签名委托与件注入/
|
||||
# 白盒入口——外部调用面与断点取数面零变化。
|
||||
|
||||
# 件注入(建件与宿主引用换代时调用——钱包/账类/图行/装配集为共享引用,出账直接落同一引用)
|
||||
# 件注入(建件与宿主引用换代时调用——钱包/主角账三件/图行/装配集为共享引用,出账直接落同一引用)
|
||||
func _bind_shop() -> void:
|
||||
_shop.bind({
|
||||
"wallet": _ledger,
|
||||
"run_ledger": run_ledger,
|
||||
"wallet": _wallet,
|
||||
"journal": _journal,
|
||||
"level_row": level_row,
|
||||
"tree_rules": _tree_rules,
|
||||
"attunement": _attunement,
|
||||
@@ -774,7 +745,7 @@ func _emit_wallet_event(event: String, gp_now: int, extra: Dictionary) -> void:
|
||||
|
||||
# 购血(商店 max_hp 商品——当前生命 +Δ 并封顶新上限,与账本置满/关卡完成回血同式)
|
||||
func _gain_max_hp_flat(flat: int) -> void:
|
||||
hero_hp = mini(hero_hp + flat, effective_max_hp())
|
||||
_stats.hero_hp = mini(_stats.hero_hp + flat, _stats.effective_max_hp())
|
||||
|
||||
|
||||
func shop_products() -> Array:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
## 刷新账件:三选一刷新三源账唯一所有者(X 刷新件每碰三选一重给/优先消耗不结转、Y 营地升级行
|
||||
## 冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久)+GP 付费刷计数与报价公式
|
||||
## (ceil(商店基价×曲线^本次停点已付费刷)——与商店货架刷新同两个调参旋钮,单点调参)。
|
||||
## 零停点态依赖:免费/付费支出的编排归 CurioStop(查停点态→本件支出→重抽重发),本件只记账。
|
||||
## 断点自交片段(refresh_stock/attune_refresh_y 两键,Y 缺键回退参数面)——拼盘落盘归冒险应用层。
|
||||
## Core 层零 UI;随机零消费。
|
||||
class_name RefreshBank
|
||||
extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var _refresh_x := 0 # 刷新件余量(每碰三选一重置=X 表值;优先消耗、不结转)
|
||||
var _refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
|
||||
var _paid_refreshes := 0 # 本次停点已付费刷新次数(对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
|
||||
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _refresh_x**:X 每停点重置会吞存量=写错账缺陷 F-1)
|
||||
var _refresh_x_cap := 0 # 刷新件余量基数(装配集 effect_kinds 含 bonus_refresh 的行 magnitude 合计——
|
||||
# 宿主冒险开始时预算注入;装配集冒险期间锁死〔契约 8〕故预算与逐停点查询等价)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb) -> void:
|
||||
db = db_ref
|
||||
|
||||
|
||||
# 刷新件基数注入(宿主 start_adventure/resume 时点计算——X 每碰三选一重置回此基数)
|
||||
func set_refresh_x_cap(total: int) -> void:
|
||||
_refresh_x_cap = maxi(0, total)
|
||||
|
||||
|
||||
# X 每碰三选一重置回基数(不结转——宿主停点开启时点调用)
|
||||
func reset_x() -> void:
|
||||
_refresh_x = _refresh_x_cap
|
||||
|
||||
|
||||
# Y 池注入(冒险开始/续玩参数面——宿主单写)
|
||||
func set_refresh_y(v: int) -> void:
|
||||
_refresh_y = maxi(0, v)
|
||||
|
||||
|
||||
func refresh_y() -> int:
|
||||
return _refresh_y
|
||||
|
||||
|
||||
# 刷新存量账加仓(refresh_stock_grant 装配即得一次性加仓——只入存量账、临时账不动=V8 契约)
|
||||
func add_refresh_stock(n: int) -> void:
|
||||
_refresh_stock += n
|
||||
|
||||
|
||||
# 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费)
|
||||
func refresh_stock() -> int:
|
||||
return _refresh_stock
|
||||
|
||||
|
||||
# 当前可刷新次数(X 优先 + Y 池 + 刷新存量账——呈现层余量读数口)
|
||||
func free_left() -> int:
|
||||
return _refresh_x + _refresh_y + _refresh_stock
|
||||
|
||||
|
||||
# 免费位消耗(X→Y→存量消耗序逐字;三源全空 false 零副作用)
|
||||
func spend_free() -> bool:
|
||||
# 免费位消耗序:X(刷新件每停点重给,优先消耗)→ Y(营地冒险周期池)→ 存量账(刷新券加仓,
|
||||
# 跨停点持久)
|
||||
if _refresh_x <= 0 and _refresh_y <= 0 and _refresh_stock <= 0:
|
||||
return false
|
||||
if _refresh_x > 0:
|
||||
_refresh_x -= 1
|
||||
elif _refresh_y > 0:
|
||||
_refresh_y -= 1
|
||||
else:
|
||||
_refresh_stock -= 1
|
||||
return true
|
||||
|
||||
|
||||
# 付费刷计数递增(对齐商店 MC-1——免费位 X/Y 用尽后转付费,同旋钮同曲线同钱包)
|
||||
func charge_paid() -> void:
|
||||
_paid_refreshes += 1
|
||||
|
||||
|
||||
# 付费刷计数随停点清零(对齐商店进店清零)
|
||||
func reset_paid() -> void:
|
||||
_paid_refreshes = 0
|
||||
|
||||
|
||||
# 本次 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))))
|
||||
|
||||
|
||||
# 冒险级复位(宿主 reset_adventure_state 调用——刷新三源与付费计数全零)
|
||||
func reset_all() -> void:
|
||||
_refresh_x = 0
|
||||
_refresh_y = 0
|
||||
_paid_refreshes = 0
|
||||
_refresh_stock = 0
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)----
|
||||
# 片段键:refresh_stock/attune_refresh_y
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"refresh_stock": _refresh_stock,
|
||||
"attune_refresh_y": _refresh_y,
|
||||
}
|
||||
|
||||
|
||||
# 片段回填(刷新存量/Y 池。存量缺键缺省 0(clamp 类);Y 余量优先读断点(花掉不回血=堵
|
||||
# 「用完退菜单再续玩满血复活」可利用漏洞);缺键=旧版断点,回退参数面整额注入(旧档行为零变化)
|
||||
# ——params_fallback 类)
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void:
|
||||
_refresh_stock = maxi(0, int(frag.get("refresh_stock", 0))) # 旧档缺键缺省 0(clamp 类)
|
||||
_refresh_y = maxi(0, int(frag.get("attune_refresh_y", int(params.get("attune_refresh_y", 0)))))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cvh8xf2u44xcg
|
||||
+19
-12
@@ -13,8 +13,8 @@
|
||||
## 类变量封闭面(15≤15):bundle 11 项(db/reward_economy/damage/fx_host/action_economy/status_effects/
|
||||
## units/emit_cb/fatal_cb/attune_safe_seen_cb/pool_effective_cb,缺一键 assert)+escrow+defer
|
||||
## (键集封闭 on/queue——原 _defer_board_damage/_deferred_board 二合一)+treasure_inventory
|
||||
## (经宿主 setter 转发注入——库存注入时序在宿主构造之后)+battle_ledger(bind_battle 挂接——
|
||||
## use_treasure 充能扣减与真视计数消费)。禁实现期增删键。
|
||||
## (经宿主 setter 转发注入——库存注入时序在宿主构造之后)+battle_hooks(bind_battle 挂接回调束——
|
||||
## use_treasure 充能消耗/真视计数与负向踩雷计数消费;宿主侧账本单点留编排器)。禁实现期增删键。
|
||||
class_name RewardGrant
|
||||
extends RefCounted
|
||||
|
||||
@@ -32,7 +32,7 @@ var pool_effective_cb: Callable # 宿主 _pool_effective——percent 行池
|
||||
var escrow := 0 # 战斗内 GP 缓冲(战斗域中间态——reset_battle 清零;gp_gain/gp_loss 兑付累记;随 gp_dropped 上抛即弃,不入钱包)
|
||||
var defer := {"on": false, "queue": []} # 案C 暂存闸(授予段伤害单据只暂存不结账——战斗段怪物还手后按事件序补提交统一结账)
|
||||
var treasure_inventory: TreasureItems = null # 经宿主 setter 转发注入(库存注入时序在宿主构造之后)
|
||||
var battle_ledger: RunLedger = null # begin_battle 账本引用挂接(bind_battle——缺省新账面由宿主传入)
|
||||
var battle_hooks: Dictionary[Variant, Variant] = {} # begin_battle 战斗域回调束(bind_battle 挂接——键集封闭三键:attune_consume/vision_add/negative_blast;宿主转账本三单点)
|
||||
|
||||
|
||||
func _init(bundle: Dictionary) -> void:
|
||||
@@ -52,9 +52,12 @@ func _init(bundle: Dictionary) -> void:
|
||||
pool_effective_cb = bundle.pool_effective_cb
|
||||
|
||||
|
||||
# begin_battle 账本引用挂接(编排器 begin_battle 调用——attune_charge_consume/vision_count_add 消费)
|
||||
func bind_battle(ledger_ref: RunLedger) -> void:
|
||||
battle_ledger = ledger_ref
|
||||
# begin_battle 战斗域回调束挂接(编排器传三键 Callable——充能消耗/真视计数/负向踩雷计数;
|
||||
# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,本件对账本零强引用)
|
||||
func bind_battle(hooks: Dictionary) -> void:
|
||||
for k in ["attune_consume", "vision_add", "negative_blast"]:
|
||||
assert(hooks.has(k), "[奖励授予] 战斗域回调束缺键: %s" % k)
|
||||
battle_hooks = hooks.duplicate()
|
||||
|
||||
|
||||
# begin_battle 运行态复位(escrow 清零+案C 闸复位——每战编排器新建本件天然全新,复位为防御性收口)
|
||||
@@ -98,15 +101,15 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
|
||||
if not reject.is_empty():
|
||||
return reject # ③ 拒绝零消耗(④ consume 之前 return——reason 可呈现)
|
||||
var rec: Dictionary[Variant, Variant] = treasure_inventory.consume(handle)
|
||||
# 同调主动件充能扣减(共享账本直改:adventure/level/settlement 同一字典引用)
|
||||
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement":
|
||||
battle_ledger.attune_charge_consume(String(rec.get("item_id", "")))
|
||||
# 同调主动件充能消耗(单源化——行标 used 幂等承载,经宿主回调束转 AttuneLoadout.consume_charge)
|
||||
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement" and battle_hooks.has("attune_consume"):
|
||||
(battle_hooks["attune_consume"] as Callable).call(String(rec.get("item_id", "")))
|
||||
# 真视类工具使用计数(信标网络解锁行为条件:受理成功恰计一次;
|
||||
# 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used)
|
||||
# 经宿主回调束转 GainJournal.record_vision_use,冒险收束聚合入档 save.add_vision_used)
|
||||
var kinds: Array[Variant] = v.kinds
|
||||
if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"):
|
||||
if not battle_ledger.vision_counter.is_empty():
|
||||
battle_ledger.vision_count_add(1) # 受理成功恰计一次(缺省空账=调试不计)
|
||||
if battle_hooks.has("vision_add"):
|
||||
(battle_hooks["vision_add"] as Callable).call() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义)
|
||||
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
|
||||
|
||||
|
||||
@@ -302,6 +305,10 @@ func _flat_action(row: Dictionary, display: String, att: Dictionary) -> String:
|
||||
|
||||
# 群伤单据(无类型恒定表值——打全体存活敌)
|
||||
func _flat_blast(row: Dictionary, display: String, att: Dictionary, sync: bool) -> String:
|
||||
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
|
||||
# 极性判定在此传点,宿主只记账)
|
||||
if String(row.get("polarity", "positive")) == "negative" and battle_hooks.has("negative_blast"):
|
||||
(battle_hooks["negative_blast"] as Callable).call()
|
||||
var t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, String(att.get("source_class", "")), display,
|
||||
{"channel": "board", "base_value": int(row.power), "target_rule": DamageController.TARGET_ALL_ALIVE,
|
||||
"ri": int(att.get("ri", 0)), "origin_cell": att.get("cell", Vector2i(-1, -1)),
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
class_name RunLedger
|
||||
extends RefCounted
|
||||
# 共享四本账显式承载(审计 P1-4「共享可变字典跨三层:所有权名义单点、实际三处
|
||||
# 可写」收口 = 单类持四账)。
|
||||
# 所有权:AdventureRunner 持有(开局构造、冒险全程唯一宿主);LevelRunner 经 set_run_ledger
|
||||
# 下传并装配 encounter;战斗侧(BattleOrchestrator)经 encounter["ledger"] 拿本类引用,
|
||||
# 只走下方 read/append 原语;adventure 桶经 EffectContext 引用被效果引擎就地直写(无战末 export/
|
||||
# 开局 seed 搬运时点——跨战持久=同一字典引用,随本类生灭)。
|
||||
var attune_charges: Dictionary[Variant, Variant] = {} # 同调主动件充能账(use_treasure 扣减;断点随局回档)
|
||||
var vision_counter: Dictionary[Variant, Variant] = {} # 真视类使用计数(冒险开始置 {"used":0};
|
||||
# 缺省空 = 调试/单场战斗不计——enc 缺键守卫语义)
|
||||
var adventure_counters: Dictionary[Variant, Variant] = {} # adventure 桶(adv_ 前缀计数器;经 EffectContext 引用被引擎就地直写、跨战持久随本类生灭)
|
||||
var bonus: Dictionary[Variant, Variant] = {} # 统一冒险加算区(冒险开始置四轴零账——商店购买/三选一属性件/战末 bonus_delta 汇入)
|
||||
|
||||
|
||||
# ---- 战斗侧原语(裁点②甲:战斗侧只拿 read/append 两族;宿主侧冒险/关卡域直读字段域) ----
|
||||
|
||||
func attune_charge_has(item_id: String) -> bool:
|
||||
return attune_charges.has(item_id)
|
||||
|
||||
|
||||
func attune_charge_set(item_id: String, value: int) -> void:
|
||||
attune_charges[item_id] = value
|
||||
|
||||
|
||||
func attune_charge_consume(item_id: String) -> void:
|
||||
# 原编排器 use_treasure 扣减语义逐字(has 闸 + maxi(0,·) 下限)
|
||||
if attune_charges.has(item_id):
|
||||
attune_charges[item_id] = maxi(0, int(attune_charges[item_id]) - 1)
|
||||
|
||||
|
||||
func vision_count_add(n: int) -> void:
|
||||
vision_counter["used"] = int(vision_counter.get("used", 0)) + n
|
||||
|
||||
|
||||
func bonus_add_axis(axis: String, delta: int) -> void:
|
||||
# 原编排器战末汇入逐字语义(has 闸——账外轴丢弃)
|
||||
if bonus.has(axis):
|
||||
bonus[axis] = int(bonus.get(axis, 0)) + delta
|
||||
@@ -1 +0,0 @@
|
||||
uid://7afr7e1gtrj5
|
||||
+15
-13
@@ -1,6 +1,6 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 商店会话件:关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主。
|
||||
## 账本/钱包/装配集归宿主 LevelRunner(共享引用经 bind 注入,出账直接落同一引用改写宿主账本);
|
||||
## 钱包/主角账/装配集归宿主(GpWallet/GainJournal 共享引用经 bind 注入——GP 读写走钱包、购买加算走日记单点);
|
||||
## Core 层零 UI 依赖;公开面由 LevelRunner 同签名委托承载(外部调用面零变化);
|
||||
## legacy 模式零 RNG 零池消费(off 对照承载)。
|
||||
class_name ShopSession
|
||||
@@ -8,9 +8,8 @@ extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
var _refs: Dictionary[Variant, Variant] = {} # wallet/run_ledger/level_row/tree_rules/attunement 宿主共享引用绑定
|
||||
var _refs: Dictionary[Variant, Variant] = {} # hero/level_row/tree_rules/attunement 宿主共享引用绑定
|
||||
var _hooks: Dictionary[Variant, Variant] = {} # wallet_emit/max_hp_gain/battle 宿主回调绑定
|
||||
var purchased: Dictionary[Variant, Variant] = {} # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||
var pool_mode := false # pool 模式封印(set_shop_pool 注入时置位)
|
||||
var pool_deck: Array[Variant] = [] # 共享池令牌库共享引用(库存=剩余令牌数单点)
|
||||
var pool_consume := Callable() # 池消费 handler(跨渠道共库兑付)
|
||||
@@ -64,7 +63,7 @@ func _spend_discounted(price: int) -> int:
|
||||
|
||||
|
||||
func purchased_tier(product_id: String) -> int:
|
||||
return int(purchased.get(product_id, 0))
|
||||
return _journal().purchase_tier(product_id)
|
||||
|
||||
|
||||
# 购买(未知商品/属性键出域/余额不足拒绝——返回 false 零副作用):
|
||||
@@ -75,7 +74,7 @@ func buy_product(product_id: String) -> bool:
|
||||
push_error("[关卡节点地图] 商店房未知商品: %s" % product_id)
|
||||
return false
|
||||
var attr := String(prow.get("attr", ""))
|
||||
if not _bonus().has(attr):
|
||||
if not GainJournal.BONUS_AXES.has(attr):
|
||||
push_error("[关卡节点地图] 商品属性键出域: %s(attr=%s)" % [product_id, attr])
|
||||
return false
|
||||
var price := current_price(prow)
|
||||
@@ -84,11 +83,10 @@ func buy_product(product_id: String) -> bool:
|
||||
return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝+静默留痕)
|
||||
_set_gp(_gp() - price)
|
||||
var flat := int(prow.get("flat_value", 0))
|
||||
var bonus := _bonus()
|
||||
bonus[attr] = int(bonus[attr]) + flat
|
||||
_journal().gain_bonus(attr, flat, "shop_product", product_id) # 加算区单写原语(明细+汇总同拍——wr1-s 同值承载)
|
||||
if attr == "max_hp":
|
||||
(_hooks["max_hp_gain"] as Callable).call(flat) # 购血:上限与当前生命同步 +Δ(封顶新上限)
|
||||
purchased[product_id] = purchased_tier(product_id) + 1
|
||||
_journal().record_purchase(product_id)
|
||||
_emit("buy", {"product": product_id, "price": price})
|
||||
return true
|
||||
|
||||
@@ -309,18 +307,22 @@ func shop_bp_visit_buys() -> int:
|
||||
return visit_buys
|
||||
|
||||
|
||||
# ---- 宿主引用读取(共享引用绑定面——钱包/账类/图行/装配集)----
|
||||
# ---- 宿主引用读取(共享引用绑定面——钱包/加算日记/图行/装配集)----
|
||||
|
||||
func _gp() -> int:
|
||||
return int((_refs["wallet"] as Dictionary).get("gp", 0))
|
||||
return _wallet().gp
|
||||
|
||||
|
||||
func _set_gp(v: int) -> void:
|
||||
(_refs["wallet"] as Dictionary)["gp"] = v
|
||||
_wallet().gp = v
|
||||
|
||||
|
||||
func _bonus() -> Dictionary:
|
||||
return (_refs["run_ledger"] as RunLedger).bonus
|
||||
func _wallet() -> GpWallet:
|
||||
return _refs["wallet"] as GpWallet
|
||||
|
||||
|
||||
func _journal() -> GainJournal:
|
||||
return _refs["journal"] as GainJournal
|
||||
|
||||
|
||||
func _level_index() -> int:
|
||||
|
||||
+82
-28
@@ -80,20 +80,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
|
||||
var eovr: int = int(enemy_atk_overrides[row_i]) if row_i < enemy_atk_overrides.size() else -1
|
||||
for i in range(per_row_counts[row_i]):
|
||||
var eff_row_atk: int = eovr if eovr >= 0 else int(row.atk)
|
||||
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条快照纪律)
|
||||
var pool_snap: Array[Dictionary] = []
|
||||
for entry in row.get("pool", []):
|
||||
var snap_entry := {
|
||||
"skill_id": String(entry.get("skill_id", "")),
|
||||
"weight": int(entry.get("weight", 0)),
|
||||
"cooldown_left": 0,
|
||||
}
|
||||
# Boss 半血解锁技(unlock_hp_pct 随池模板快照进实例池;缺省无字段=无门槛;
|
||||
# unlocked=单向闩标记,初始锁定)
|
||||
if entry.has("unlock_hp_pct"):
|
||||
snap_entry["unlock_hp_pct"] = int(entry.get("unlock_hp_pct", 0))
|
||||
snap_entry["unlocked"] = false
|
||||
pool_snap.append(snap_entry)
|
||||
var pool_snap: Array[Dictionary] = _pool_snapshot(row) # 实例池快照(开战模板快照——纪律见助手头注)
|
||||
_enemies.append({
|
||||
"handle_id": handle,
|
||||
"enemy_id": String(row.enemy_id),
|
||||
@@ -117,6 +104,26 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
|
||||
return true
|
||||
|
||||
|
||||
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条
|
||||
# 快照纪律。触发条件:技能行 trigger 是字段权威——快照随读;带 latch 行写单向闩标记
|
||||
# trigger_latched 初始锁定;缺省无 trigger=无条件技)
|
||||
func _pool_snapshot(row: Dictionary) -> Array[Dictionary]:
|
||||
var pool_snap: Array[Dictionary] = []
|
||||
for entry in row.get("pool", []):
|
||||
var snap_entry := {
|
||||
"skill_id": String(entry.get("skill_id", "")),
|
||||
"weight": int(entry.get("weight", 0)),
|
||||
"cooldown_left": 0,
|
||||
}
|
||||
var srow_snap: Dictionary[Variant, Variant] = db.skill_row(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)):
|
||||
snap_entry["trigger_latched"] = false
|
||||
pool_snap.append(snap_entry)
|
||||
return pool_snap
|
||||
|
||||
|
||||
func query_alive(side: String) -> Array:
|
||||
if _despawned:
|
||||
_diag("Despawned 句柄查询(域闭)")
|
||||
@@ -132,21 +139,69 @@ func query_alive(side: String) -> Array:
|
||||
|
||||
# ---- 实例池状态机(全部确定性状态,随机在 战斗结算换算层)----
|
||||
|
||||
# 池条目锁定判定(Boss 半血解锁——带 unlock_hp_pct 且未过闩 = 锁定:双面过滤不进
|
||||
# 可摇集合与保底特殊技集合;解锁后即可摇,冷却初值 0)
|
||||
func _entry_locked(e: Dictionary) -> bool:
|
||||
return e.has("unlock_hp_pct") and not bool(e.get("unlocked", false))
|
||||
# 池条目门槛判定(触发条件收编——无 trigger 恒 false;闩已过恒 false〔过闩即不再拦〕;
|
||||
# 否则按 fact 分派求值。百分比事实=阈值式(v1.2):先折算绝对阈值 T=round(对应 max_hp × value/100)、
|
||||
# 再以 hp op T 比较——六算子统一在该阈值上定义。facts 缺键=条件假(拦)——fail-safe)
|
||||
func _entry_blocked(e: Dictionary, u: Dictionary, facts: Dictionary) -> bool:
|
||||
if not e.has("trigger"):
|
||||
return false
|
||||
if e.has("trigger_latched") and bool(e.get("trigger_latched", false)):
|
||||
return false # 闩已过——不再拦(治疗抬血不回锁)
|
||||
var t: Dictionary[Variant, Variant] = e.get("trigger", {})
|
||||
var fact := String(t.get("fact", ""))
|
||||
var op := String(t.get("op", ""))
|
||||
var value: Variant = t.get("value", 0)
|
||||
if fact == "self_hp_pct" or fact == "hero_hp_pct":
|
||||
return _pct_fact_blocked(fact, value, op, u) # 自类/主角百分比事实(阈值式折算——v1.2)
|
||||
# 全局事实(编排器组包——缺键=拦)
|
||||
if not facts.has(fact):
|
||||
return true
|
||||
var fv: Variant = facts.get(fact)
|
||||
if fact == "last_round_end":
|
||||
return not _compare_str(String(fv), op, String(value))
|
||||
return not _compare(int(fv), op, int(value))
|
||||
|
||||
|
||||
# 六算子统一比较(数值)
|
||||
func _compare(lhs: int, op: String, rhs: int) -> bool:
|
||||
match op:
|
||||
"eq": return lhs == rhs
|
||||
"neq": return lhs != rhs
|
||||
"gt": return lhs > rhs
|
||||
"gte": return lhs >= rhs
|
||||
"lt": return lhs < rhs
|
||||
"lte": return lhs <= rhs
|
||||
return false
|
||||
|
||||
|
||||
# 枚举算子(eq/neq——字符串)
|
||||
func _compare_str(lhs: String, op: String, rhs: String) -> bool:
|
||||
return lhs == rhs if op == "eq" else lhs != rhs
|
||||
|
||||
|
||||
# 可摇集合:冷却已尽的池条目投影 [{skill_id, weight}]。普通攻击冷却恒 0 → 恒非空(空池不可能的结构前提,
|
||||
# 加载校验"每池恰一行普攻"兜底)。hero/失效句柄拒绝。锁定条目(半血未解锁)双面过滤不入。
|
||||
func rollable_skills(u: Dictionary) -> Array:
|
||||
# 百分比事实阈值判定(阈值式 v1.2:先折算绝对阈值 T=round(对应 max_hp × value/100)、
|
||||
# 再以 hp op T 比较——六算子统一在该阈值上定义。self=容器内直读单位行;hero=主角行,
|
||||
# 无主角行=条件假(拦——fail-safe))
|
||||
func _pct_fact_blocked(fact: String, value: Variant, op: String, u: Dictionary) -> bool:
|
||||
if fact == "self_hp_pct":
|
||||
var threshold: int = int(round(float(int(u.max_hp)) * float(int(value)) / 100.0))
|
||||
return not _compare(int(u.hp), op, threshold)
|
||||
var hrow: Dictionary[Variant, Variant] = _hero
|
||||
if hrow.is_empty():
|
||||
return true # 无主角行——条件假(fail-safe)
|
||||
var hthreshold: int = int(round(float(int(hrow.max_hp)) * float(int(value)) / 100.0))
|
||||
return not _compare(int(hrow.hp), op, hthreshold)
|
||||
|
||||
|
||||
func rollable_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
_diag("rollable_skills 失效句柄")
|
||||
return []
|
||||
var out: Array[Dictionary] = []
|
||||
for e in u.pool:
|
||||
if _entry_locked(e):
|
||||
if _entry_blocked(e, u, facts):
|
||||
continue
|
||||
if int(e.cooldown_left) <= 0:
|
||||
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
|
||||
@@ -155,13 +210,13 @@ func rollable_skills(u: Dictionary) -> Array:
|
||||
|
||||
# 特殊技集合(保底专用——proposal §2.1:必出特殊技、特殊技内按权重、不受冷却限制):type ≠ 普攻的全部池条目。
|
||||
# 锁定条目(半血未解锁)双面过滤——保底不得逼出未解锁技。
|
||||
func special_skills(u: Dictionary) -> Array:
|
||||
func special_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
_diag("special_skills 失效句柄")
|
||||
return []
|
||||
var out: Array[Dictionary] = []
|
||||
for e in u.pool:
|
||||
if _entry_locked(e):
|
||||
if _entry_blocked(e, u, facts):
|
||||
continue
|
||||
var srow: Dictionary[Variant, Variant] = db.skill_row(String(e.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) != "basic":
|
||||
@@ -172,16 +227,15 @@ func special_skills(u: Dictionary) -> Array:
|
||||
# 半血解锁闩检查(开轮摇号前逐敌检查;hp ≤ round(max_hp × unlock_hp_pct/100) →
|
||||
# 一次过门、单向闩〔实例标记,不解锁回退〕。门=确定性谓词零随机;返回本拍新解锁的 skill_id 列表
|
||||
# 供编排层出归因行〔每技恰一次——闩已置位不再返回〕)
|
||||
func check_skill_unlocks(u: Dictionary) -> Array:
|
||||
func check_trigger_unlocks(u: Dictionary, facts: Dictionary = {}) -> Array:
|
||||
var out: Array[Variant] = []
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false) or not bool(u.get("alive", false)):
|
||||
return out
|
||||
for e in u.pool:
|
||||
if not e.has("unlock_hp_pct") or bool(e.get("unlocked", false)):
|
||||
continue
|
||||
var threshold: int = int(round(float(int(u.max_hp)) * float(int(e.unlock_hp_pct)) / 100.0))
|
||||
if int(u.hp) <= threshold:
|
||||
e["unlocked"] = true # 单向闩(不解锁回退——治疗抬血不解锁)
|
||||
if not e.has("trigger") or not e.has("trigger_latched") or bool(e.get("trigger_latched", false)):
|
||||
continue # 只处理闩型且未过闩条目
|
||||
if not _entry_blocked(e, u, facts):
|
||||
e["trigger_latched"] = true # 单向闩(不解锁回退——治疗抬血不回锁)
|
||||
out.append(String(e.skill_id))
|
||||
return out
|
||||
|
||||
|
||||
@@ -108,12 +108,6 @@ func validate() -> Array:
|
||||
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]})
|
||||
# unlock_hp_pct(可省略——Boss 半血解锁技:缺省无字段=无门槛;域 [1,99] percent。
|
||||
# 投放纪律=仅 Boss 行〔enemy 表无 boss 编码位,硬闸须跨表查节点类型——不做硬闸,文档纪律登记〕)
|
||||
if entry.has("unlock_hp_pct"):
|
||||
var uhp: int = int(entry.get("unlock_hp_pct", 0))
|
||||
if uhp < 1 or uhp > 99:
|
||||
errs.append({"category": "domain", "message": "%s pool unlock_hp_pct 越域 [1,99]: %d" % [eid, uhp]})
|
||||
# armor_type(可省略——缺省 light 向后兼容)
|
||||
var at: String = String(row.get("armor_type", "light"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
|
||||
@@ -11,6 +11,13 @@ extends Resource
|
||||
@export var pity_threshold: int = 4
|
||||
|
||||
const SKILL_TYPES := ["basic", "damage", "buff", "debuff"] # 普通攻击 / 伤害 / 增益 / 妨害(封闭枚举四型)
|
||||
# trigger(触发条件)封闭词表(敌人系统 v1.2「触发条件」节——出域加载期拒绝止动):
|
||||
const TRIGGER_FACTS := ["self_hp_pct", "hero_hp_pct", "round_index", "last_round_end", "mines_hit_last", "mines_hit_total"]
|
||||
const TRIGGER_OPS := ["eq", "neq", "gt", "gte", "lt", "lte"]
|
||||
const TRIGGER_KEYS := ["fact", "op", "value", "latch"]
|
||||
const TRIGGER_ENUM_FACTS := ["last_round_end"] # 枚举事实(op 限域 eq/neq;value 限枚举串)
|
||||
const TRIGGER_ENUM_VALUES := ["none", "cleared", "stopped", "ap_exhausted"]
|
||||
const TRIGGER_PCT_FACTS := ["self_hp_pct", "hero_hp_pct"] # 百分比事实(value 域 [1,99]——阈值式求值)
|
||||
const MULT_DOMAIN_MIN := 1.1 # damage_mult 域 [1.1, 16] provisional(proposal §2.2)
|
||||
const MULT_DOMAIN_MAX := 16.0
|
||||
const RIDER_MULT_MAX := 1.0 # rider_mult 域 (0, 1](必中附加分量——proposal-status-vocabulary §2;首发 0.75 锚 AC 折扣档最轻档)
|
||||
@@ -37,6 +44,9 @@ func validate() -> Array:
|
||||
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:
|
||||
@@ -44,14 +54,7 @@ func validate() -> Array:
|
||||
# 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2)
|
||||
if type == "basic":
|
||||
has_basic = true
|
||||
if row.has("damage_mult") and not is_equal_approx(float(row.damage_mult), 1.0):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻 damage_mult ≠ 1(禁配——要配就是伤害技): %s" % [sid, str(row.damage_mult)]})
|
||||
if row.has("cooldown_beats") and cd != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
_validate_basic_row(sid, row, cd, errs)
|
||||
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
|
||||
# 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
|
||||
if type == "damage":
|
||||
@@ -97,6 +100,64 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
# trigger 域校验(敌人系统 v1.2 触发条件:四键白名单/词表出域/类型分派——枚举事实算子限域 eq/neq
|
||||
# 且 value 限枚举串、百分比事实 value 域 [1,99]、数值事实 value 须整数、latch 须布尔。行缺 trigger
|
||||
# 直返——可省略字段)
|
||||
func _validate_trigger(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if not row.has("trigger"):
|
||||
return
|
||||
var trig: Variant = row.get("trigger", null)
|
||||
if not (trig is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s trigger 须为 {fact,op,value,latch} 字典" % sid})
|
||||
return
|
||||
var t: Dictionary[Variant, Variant] = trig
|
||||
for tk in t.keys():
|
||||
if not TRIGGER_KEYS.has(String(tk)):
|
||||
errs.append({"category": "domain", "message": "%s trigger 键白名单外: %s" % [sid, str(tk)]})
|
||||
_validate_trigger_domain(t, sid, errs)
|
||||
|
||||
|
||||
# trigger fact/op/value/latch 域校验(词表出域与类型分派——报错顺序=报告顺序,分派段单列)
|
||||
func _validate_trigger_domain(t: Dictionary[Variant, Variant], sid: String, errs: Array[Dictionary]) -> void:
|
||||
var tf := String(t.get("fact", ""))
|
||||
var top := String(t.get("op", ""))
|
||||
if not TRIGGER_FACTS.has(tf):
|
||||
errs.append({"category": "enum", "message": "%s trigger fact 出域: %s" % [sid, tf]})
|
||||
if not TRIGGER_OPS.has(top):
|
||||
errs.append({"category": "enum", "message": "%s trigger op 出域: %s" % [sid, top]})
|
||||
if TRIGGER_ENUM_FACTS.has(tf) and not (top == "eq" or top == "neq"):
|
||||
errs.append({"category": "domain", "message": "%s trigger 枚举事实 %s 算子限域 eq/neq: %s" % [sid, tf, top]})
|
||||
_validate_trigger_value(sid, t, tf, errs)
|
||||
if t.has("latch") and not (t.get("latch", false) is bool):
|
||||
errs.append({"category": "domain", "message": "%s trigger latch 须布尔: %s" % [sid, str(t.get("latch", false))]})
|
||||
|
||||
|
||||
# trigger value 类型分派校验(枚举事实 value 限枚举串/百分比事实 value 域 [1,99]/数值事实 value 须整数)
|
||||
func _validate_trigger_value(sid: String, t: Dictionary[Variant, Variant], tf: String, errs: Array[Dictionary]) -> void:
|
||||
if tf != "" and TRIGGER_ENUM_FACTS.has(tf):
|
||||
var ev = t.get("value", "")
|
||||
if not (ev is String) or not TRIGGER_ENUM_VALUES.has(String(ev)):
|
||||
errs.append({"category": "domain", "message": "%s trigger 枚举事实 value 越域: %s(域 %s)" % [sid, str(ev), str(TRIGGER_ENUM_VALUES)]})
|
||||
elif TRIGGER_PCT_FACTS.has(tf):
|
||||
var pv := int(t.get("value", 0))
|
||||
if pv < 1 or pv > 99:
|
||||
errs.append({"category": "domain", "message": "%s trigger 百分比事实 value 越域 [1,99]: %d" % [sid, pv]})
|
||||
elif tf != "" and not (t.get("value", 0) is int):
|
||||
errs.append({"category": "domain", "message": "%s trigger 数值事实 value 须整数: %s" % [sid, str(t.get("value", 0))]})
|
||||
|
||||
|
||||
# 普攻行禁配校验(倍率恒 1、冷却恒 0、不带增益/rider 字段——「池恒非空」的结构前提)
|
||||
func _validate_basic_row(sid: String, row: Dictionary, cd: int, errs: Array[Dictionary]) -> void:
|
||||
if row.has("damage_mult") and not is_equal_approx(float(row.damage_mult), 1.0):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻 damage_mult ≠ 1(禁配——要配就是伤害技): %s" % [sid, str(row.damage_mult)]})
|
||||
if row.has("cooldown_beats") and cd != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
|
||||
|
||||
func row_by_id(skill_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if row.get("skill_id", "") == skill_id:
|
||||
|
||||
+18
-11
@@ -22,6 +22,9 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "lunge",
|
||||
"weight": 1
|
||||
}, {
|
||||
"skill_id": "desperate_guard",
|
||||
"weight": 1
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -44,6 +47,9 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "lunge",
|
||||
"weight": 1
|
||||
}, {
|
||||
"skill_id": "desperate_guard",
|
||||
"weight": 1
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -145,7 +151,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "quake_smash",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -173,7 +178,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "quake_smash",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -413,7 +417,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "venom_curb",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "ward",
|
||||
@@ -444,7 +447,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "venom_curb",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "ward",
|
||||
@@ -496,6 +498,9 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"weight": 1
|
||||
}, {
|
||||
"skill_id": "sneak_ambush",
|
||||
"weight": 2
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -521,6 +526,9 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"weight": 1
|
||||
}, {
|
||||
"skill_id": "sneak_ambush",
|
||||
"weight": 2
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -573,6 +581,9 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"weight": 1
|
||||
}, {
|
||||
"skill_id": "sneak_ambush",
|
||||
"weight": 2
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
@@ -656,7 +667,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "heavy",
|
||||
@@ -684,7 +694,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "heavy",
|
||||
@@ -712,7 +721,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "heavy",
|
||||
@@ -740,7 +748,6 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "heavy",
|
||||
@@ -748,7 +755,7 @@ rows = [{
|
||||
}]
|
||||
|
||||
; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照):
|
||||
; 试玩反馈 = 基线满血全通关(模拟 80%/真人 100%)——难度严重不足,主杠杆 = atk 曲线上调(承伤侧):
|
||||
; 试玩反馈 = 基线满血全冒险成功(模拟 80%/真人 100%)——难度严重不足,主杠杆 = atk 曲线上调(承伤侧):
|
||||
; 4/4/6/4/7 → 7/5/9/6/9(约 +50%);HP 结构调整(D0 24→20 缩短战斗、Boss 130→150 用户拍板可上调)。
|
||||
; D0 注:20 HP 会被首轮盲挖粉尘秒杀(12+9)——回 24(4 轮战斗/承伤 42%,有过程有痛感)。
|
||||
; DND 命名(用户指令):goblin 地精(单拍杂鱼)→ bugbear 熊地精(三拍伏击环——DND 原型即伏击手)→
|
||||
@@ -777,7 +784,7 @@ rows = [{
|
||||
; 地精 普攻 9/突刺 1(名义 EV 1.05;**整数基线 EV 1.10**——atk1 突刺落地 2:〔9×1+1×2〕÷10;2026-10-05 技能池执行批 D4 整数口径重登记);
|
||||
; 熊地精 普攻 6/蓄力 2/重击 2(EV 1.00 中性);食人魔酋长 普攻 6/重击 2/硬化 2(EV 1.00 中性;
|
||||
; 硬化 = Boss 池独占增益技——增益 ≤1 纪律下旧蓄力拍退役不进 Boss 池)。;
|
||||
; 酋长 EV 重锚(2026-10-05 技能池执行批 T5——震地怒击 unlock_hp_pct=50 半血解锁入池:前半场池 6/2/2/3,
|
||||
; 酋长 EV 重锚(2026-10-05 技能池执行批 T5——震地怒击半血解锁入池:前半场池 6/2/2/3,
|
||||
; 后半场 6/2/2/2+震地怒击 2=每拍 4.33;含闸长期速率抽样实测 1.1948,smoke EV 抽样带重锚 [1.10, 1.30];
|
||||
; 卓尔遏毒/石君王岩崩同批入池——读数判读归 T7 节奏重测〔后置挂 07,跑批待用户批准〕)
|
||||
; 含闸长期速率(冷却/保底)EV 锚按实施批 EV 抽样复核登记(proposal §2.5)。
|
||||
@@ -888,7 +895,7 @@ rows = [{
|
||||
;
|
||||
; 数值修复批·调参二(2026-10-01 新尺 MC 驱动):L1 系八行 atk 2→1(行 15/35/54/77/99/121/144/166;
|
||||
; 行 234 巨蜘蛛巢群主体=L2 域维持)——新尺读数:普通档 v3.4 死因=L1 四连战无治疗夹缝(雷伤 6/8/8
|
||||
; + 杂兵垫刀双压,L1 条件通关 1-2%,0/100 出第一关);完美档战役 50%(L1 93%)。刀位语义=
|
||||
; + 杂兵垫刀双压,L1 条件关卡完成率 1-2%,0/100 出第一关);完美档战役 50%(L1 93%)。刀位语义=
|
||||
; 「危险来自雷,不来自杂兵」:杂兵垫刀减半,雷直伤 6/8/8 维持痛感;L2/L3(atk 3 系)本刀不动。
|
||||
; 全旧带读数随新尺作废重测归本批。
|
||||
; 调参二补充:上述八行 atk 区间 1-3→0-2(中点恒等期望当量锚不变式——atk=1 需区间中点同步 1;0=掷点空刀,保随机手感)。
|
||||
|
||||
@@ -68,19 +68,37 @@ rows = [{
|
||||
"display_name": "震地怒击",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.5,
|
||||
"cooldown_beats": 4
|
||||
"cooldown_beats": 4,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 50, "latch": true}
|
||||
}, {
|
||||
"skill_id": "venom_curb",
|
||||
"display_name": "遏毒",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.0,
|
||||
"cooldown_beats": 3
|
||||
"cooldown_beats": 3,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 50, "latch": true}
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"display_name": "岩崩",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.5,
|
||||
"cooldown_beats": 4
|
||||
"cooldown_beats": 4,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 50, "latch": true}
|
||||
}, {
|
||||
"skill_id": "desperate_guard",
|
||||
"display_name": "孤注守势",
|
||||
"type": "buff",
|
||||
"buff_ref": "defensive_stance",
|
||||
"buff_duration": 2,
|
||||
"cooldown_beats": 0,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}
|
||||
}, {
|
||||
"skill_id": "sneak_ambush",
|
||||
"display_name": "趁隙偷袭",
|
||||
"type": "damage",
|
||||
"damage_mult": 1.5,
|
||||
"cooldown_beats": 4,
|
||||
"trigger": {"fact": "last_round_end", "op": "eq", "value": "stopped", "latch": false}
|
||||
}, {
|
||||
"skill_id": "venom_spit",
|
||||
"display_name": "毒吐",
|
||||
@@ -130,7 +148,7 @@ rows = [{
|
||||
; provisional 归 难度曲线与Boss。
|
||||
;
|
||||
; Boss 半血解锁技三行(2026-10-05 技能池执行批 T5,迭代14④——D7 用户裁案甲:每 Boss 一行新独占伤害技,
|
||||
; 工作值 provisional 归调参;pool 侧 unlock_hp_pct=50 门槛随 enemy.tres 三 Boss 行):
|
||||
; 工作值 provisional 归调参;pool 侧 半血解锁(trigger 收编)=50 门槛随 enemy.tres 三 Boss 行):
|
||||
; quake_smash 震地怒击 ×2.5/CD4(食人魔酋长)· venom_curb 遏毒 ×2.0/CD3(蛛化卓尔精灵)·
|
||||
; rockslide 岩崩 ×2.5/CD4(灰矮人石君王)。解锁语义/双面过滤/一次性弹出规格 = 敌人系统.md 意图呈现节。
|
||||
;
|
||||
|
||||
@@ -77,6 +77,15 @@ rows = [{
|
||||
"stack_policy": "refresh",
|
||||
"max_stack": 1
|
||||
}, {
|
||||
"effect_id": "defensive_stance",
|
||||
"display_name": "护身",
|
||||
"target_side": "enemy",
|
||||
"magnitude": -25,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 2,
|
||||
"stack_policy": "refresh",
|
||||
"max_stack": 1
|
||||
}, {
|
||||
"effect_id": "alert",
|
||||
"display_name": "警觉",
|
||||
"target_side": "enemy",
|
||||
@@ -88,7 +97,7 @@ rows = [{
|
||||
}]
|
||||
|
||||
; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发——defense_scan 实测墙轮 39%、
|
||||
; 真人局 12/12 轮零承伤满血通关):defense_up magnitude -50 → -30——叠满两层 = -60 减伤帽
|
||||
; 真人局 12/12 轮零承伤满血冒险成功):defense_up magnitude -50 → -30——叠满两层 = -60 减伤帽
|
||||
; (原拟 pool_clamp_lo=-60 被 schema 冻结域拦截,杠杆移至本表);石肤权重 24→16 随批(reward.tres)。
|
||||
; 承伤带对 C3 F4(≤20%/普通场、≤50%/Boss 场)复验责任随批;B3 GDD "-50" 文字同步随下次 B3 修订批。
|
||||
; 格面资源批(2026-09-15):新增 sapped(乏力——weaken 型负向,hero_offense 池,attack_up 镜像;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 三选一交互控制器:三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ TP0 令牌池卡片文案装配 + 秘宝显示名投影。
|
||||
## 信号连接只归组合根(curio_overlay/adventure 信号接至本类 on_* 入口);模态栈/演播后排窗/呈现刷新/fatal 经组合根回调注入,adventure/runner 现值注入。
|
||||
## 兑付写调用唯一经 adventure.pick_curio/apply_node_curio——本类零直写。
|
||||
## 兑付写调用唯一经 adventure.stop().pick_curio/apply_node_curio——本类零直写。
|
||||
## 同调装配不可变(冒险中零更换途径)——无替换子交互。
|
||||
extends RefCounted
|
||||
|
||||
@@ -69,7 +69,7 @@ func _skip_empty_stop() -> bool:
|
||||
toast.show_toast("可用战利品为空——本次三选一跳过", "info")
|
||||
var adventure = _adventure_get.call()
|
||||
if adventure != null:
|
||||
adventure.skip_empty_curio() # core 停点消化(其守卫=仅空候选放行)
|
||||
adventure.stop().skip_empty_curio() # core 停点消化(其守卫=仅空候选放行)
|
||||
if _curio_context == "reward_node":
|
||||
var runner = _runner_get.call()
|
||||
if runner != null:
|
||||
@@ -116,7 +116,7 @@ func on_presented(payload: Dictionary) -> void:
|
||||
func present_node_curio() -> void:
|
||||
var adventure = _adventure_get.call()
|
||||
_curio_context = "reward_node"
|
||||
var node_draw: Dictionary[Variant, Variant] = adventure.node_curio_candidates()
|
||||
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 未调即未挂),
|
||||
@@ -128,7 +128,7 @@ func present_node_curio() -> void:
|
||||
return
|
||||
_open_modal.call(curio_overlay)
|
||||
curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands))
|
||||
adventure.present_node_curio(node_cands)
|
||||
adventure.stop().present_node_curio(node_cands)
|
||||
_feed_refresh_display(adventure) # 刷新面投喂单点
|
||||
|
||||
|
||||
@@ -147,8 +147,8 @@ func show_pick() -> void:
|
||||
func _feed_refresh_display(adventure: AdventureRunner) -> void:
|
||||
var runner = _runner_get.call()
|
||||
var gp := int(runner.gp) if runner != null else 0
|
||||
var cost: int = adventure.curio_refresh_gp_cost()
|
||||
curio_overlay.set_refresh_display(adventure.attune_refresh_left(), cost, gp >= cost)
|
||||
var cost: int = adventure.refresh_bank().gp_cost()
|
||||
curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost)
|
||||
|
||||
|
||||
func _curio_card_texts(candidates: Array) -> Array:
|
||||
@@ -181,22 +181,22 @@ func _curio_card_texts(candidates: Array) -> Array:
|
||||
# 选择面,节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
|
||||
func on_refresh() -> void:
|
||||
var adventure = _adventure_get.call()
|
||||
if adventure == null or not adventure.curio_stop_pending():
|
||||
if adventure == null or not adventure.stop().curio_stop_pending():
|
||||
return
|
||||
var runner = _runner_get.call()
|
||||
var refreshed: bool = adventure.refresh_curio()
|
||||
var refreshed: bool = adventure.stop().refresh_curio()
|
||||
if not refreshed:
|
||||
# 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用)
|
||||
var cost: int = adventure.curio_refresh_gp_cost()
|
||||
var cost: int = adventure.refresh_bank().gp_cost()
|
||||
if runner == null or not runner.spend_gp(cost):
|
||||
toast.show_toast("GP不足——刷新失败", "warn")
|
||||
return
|
||||
refreshed = adventure.refresh_curio_paid()
|
||||
refreshed = adventure.stop().refresh_curio_paid()
|
||||
_refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面)
|
||||
if not refreshed:
|
||||
return
|
||||
if _curio_context == "reward_node":
|
||||
var st: Dictionary[Variant, Variant] = adventure.node_curio_pending_state()
|
||||
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))
|
||||
_feed_refresh_display(adventure)
|
||||
|
||||
@@ -210,14 +210,14 @@ func on_pick_confirmed(item_id: String) -> void:
|
||||
var kind := parts[0]
|
||||
var tier := parts[1] if parts.size() > 1 else ""
|
||||
var rid := parts[2] if parts.size() > 2 else ""
|
||||
var ok: bool = adventure.apply_node_curio(kind, tier, rid) if _curio_context == "reward_node" else adventure.pick_curio(kind, tier, rid)
|
||||
var ok: bool = adventure.stop().apply_node_curio(kind, tier, rid) if _curio_context == "reward_node" else adventure.stop().pick_curio(kind, tier, rid)
|
||||
if not ok:
|
||||
push_error("[bonus_interact] 三选一兑付被拒——见输出日志")
|
||||
return
|
||||
# T5——节点渠道专长凭证拿到即开:候选已被替换为专长面,本地读在途态重绘(零 emit——context 保持 reward_node,
|
||||
# 后续兑付/确认仍走节点路径回图选路)
|
||||
if _curio_context == "reward_node" and kind == "feat_ticket":
|
||||
var st2: Dictionary[Variant, Variant] = adventure.node_curio_pending_state()
|
||||
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))
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB
|
||||
# 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。
|
||||
# 呈现只读投影零核心触碰;行文格式=白名单项)
|
||||
if runner != null and level_active:
|
||||
var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 加算区住 RunLedger
|
||||
var b: Dictionary[Variant, Variant] = runner.journal().bonus_snapshot() # 加算区住加算日记
|
||||
var lines: Array[String] = []
|
||||
if int(b.get("atk", 0)) != 0:
|
||||
lines.append("攻击 %+d" % int(b.get("atk", 0)))
|
||||
@@ -310,7 +310,7 @@ static func gp_summary_text(payload: Dictionary, runner: Variant, save_manager:
|
||||
static func bonus_summary_text(runner: Variant) -> String:
|
||||
if runner == null:
|
||||
return ""
|
||||
var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 加算区住 RunLedger
|
||||
var b: Dictionary[Variant, Variant] = runner.journal().bonus_snapshot() # 加算区住加算日记
|
||||
var parts: Array[String] = []
|
||||
if int(b.get("atk", 0)) != 0:
|
||||
parts.append("攻 %+d" % int(b.get("atk", 0)))
|
||||
|
||||
@@ -4,20 +4,15 @@
|
||||
## 局态仍全局住在 run_scope 节点:本件零成员态、零信号连接新增(连接语句随原段搬运)。
|
||||
extends RefCounted
|
||||
|
||||
# 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根;断点注入同口)
|
||||
# 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根;断点注入同口)。
|
||||
# 信号直连(拆件批 T3):runner 六信号在 level_started 重取点重连(旧实例连接随引用释放消亡——
|
||||
# is_connected 守卫恰好一次);三选一信号面归停点件(每冒险一次——接线恰一次)。
|
||||
static func _wire_adventure(root: Variant) -> void:
|
||||
root.run_scope.adventure = AdventureRunner.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG))
|
||||
root.run_scope.adventure.step_row.connect(root._on_step_row)
|
||||
root.run_scope.adventure.round_started.connect(root._on_round_started)
|
||||
root.run_scope.adventure.board_changed.connect(root._on_board_changed)
|
||||
root.run_scope.adventure.node_entered.connect(root._on_runner_node)
|
||||
root.run_scope.adventure.choices_presented.connect(root._on_choices_presented)
|
||||
root.run_scope.adventure.wallet_changed.connect(root._on_wallet_changed)
|
||||
# 三选一信号族接线
|
||||
root.run_scope.adventure.curio_choice_presented.connect(root._bonus.on_presented)
|
||||
root.run_scope.adventure.curio_resolved.connect(root._bonus.on_resolved)
|
||||
root.run_scope.adventure.adventure_finished.connect(root._on_adventure_finished)
|
||||
root.run_scope.adventure.level_started.connect(root._on_adventure_level_started)
|
||||
root.run_scope.adventure.stop().curio_choice_presented.connect(root._bonus.on_presented)
|
||||
root.run_scope.adventure.stop().curio_resolved.connect(root._bonus.on_resolved)
|
||||
root.run_scope.adventure.set_save_manager(root.save_manager) # 断点承载注入(停点快照写盘;调试/模拟不经过此路径)
|
||||
|
||||
|
||||
@@ -26,19 +21,37 @@ static func _wire_adventure(root: Variant) -> void:
|
||||
# 本回调返回后才有 choices_presented 到达——入口选路层随即弹出)
|
||||
static func _on_adventure_level_started(root: Variant, level_index: int, display_name: String) -> void:
|
||||
root.run_scope.runner = root.run_scope.adventure.runner()
|
||||
# runner 信号直连(拆件批 T3——六转发族退役后的重取点重连;恰好一次断言 sc1 守卫承载)
|
||||
_connect_runner_once(root)
|
||||
# 开场行传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源:
|
||||
# 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」
|
||||
# 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp
|
||||
#(run_ledger.bonus——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件:
|
||||
#(hero 加算区——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件:
|
||||
# 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带
|
||||
var ov: Dictionary[Variant, Variant] = root.meta.hero_override()
|
||||
var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \
|
||||
or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \
|
||||
or int(root.run_scope.runner.run_ledger.bonus.get("atk", 0)) > 0 \
|
||||
or int(root.run_scope.runner.run_ledger.bonus.get("max_hp", 0)) > 0
|
||||
or root.run_scope.runner.journal().bonus_total("atk") > 0 \
|
||||
or root.run_scope.runner.journal().bonus_total("max_hp") > 0
|
||||
root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
|
||||
root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted))
|
||||
|
||||
# runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载)
|
||||
static func _connect_runner_once(root: Variant) -> void:
|
||||
var r = root.run_scope.runner
|
||||
if r == null:
|
||||
push_error("[冒险流程] runner 信号重连失败——level_started 后 runner 为空")
|
||||
return
|
||||
var pairs := [
|
||||
[r.step_row, root._on_step_row], [r.round_started, root._on_round_started],
|
||||
[r.board_changed, root._on_board_changed], [r.node_entered, root._on_runner_node],
|
||||
[r.choices_presented, root._on_choices_presented], [r.wallet_changed, root._on_wallet_changed],
|
||||
]
|
||||
for pair in pairs:
|
||||
if not (pair[0] as Signal).is_connected(pair[1]):
|
||||
(pair[0] as Signal).connect(pair[1])
|
||||
|
||||
|
||||
static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -> void:
|
||||
# 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面
|
||||
if root.run_scope.is_adventure and root.run_scope.adventure != null:
|
||||
@@ -204,9 +217,9 @@ static func _exit_level_screen(root: Variant) -> void:
|
||||
# 选路屏状态栏装配(视图零核心态拼装——architecture §1.3:读核心态→拼展示文案集中于装配器)。
|
||||
# 打开本屏/兑付确认回执后由组合根取成品喂入 apply_status(刷新时点与原视图内自刷一致)。
|
||||
static func level_map_status_payload(runner: LevelRunner) -> Dictionary:
|
||||
var atk_bonus := int(runner.run_ledger.bonus.get("atk", 0)) # 加算区住 RunLedger
|
||||
var ac_bonus := int(runner.run_ledger.bonus.get("armor_class", 0))
|
||||
var hp_bonus := int(runner.run_ledger.bonus.get("max_hp", 0))
|
||||
var atk_bonus := runner.journal().bonus_total("atk") # 加算区住加算日记(读面经 journal() 出口承接)
|
||||
var ac_bonus := runner.journal().bonus_total("armor_class")
|
||||
var hp_bonus := runner.journal().bonus_total("max_hp")
|
||||
var atk_txt := "%d" % runner.effective_atk()
|
||||
if atk_bonus > 0:
|
||||
atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块)
|
||||
|
||||
@@ -58,7 +58,7 @@ static func _debug_run_data(root: Variant) -> Dictionary:
|
||||
if r == null:
|
||||
return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"}
|
||||
return {"seed": seed_txt, "level": int(root.run_scope.adventure.level_index), "node": int(r.node_index),
|
||||
"hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(root.run_scope.adventure.keystone)}
|
||||
"hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": root.run_scope.adventure.journal().keystone_gained()}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,51 +78,7 @@ var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重
|
||||
var _palette = _TILE_PALETTE.new() # 瓦片件:装载缓存+查表+明度/墨族静态算法
|
||||
var _hero_sprite = _HERO_SPRITE.new() # 英雄精灵件:_init 挂为本件子级(tween 需在树上)
|
||||
|
||||
# ── 件成员同名转发(探针直读面零改动:读 _seal_tex、读写 _hero_tex_idle;其余为随迁状态单点再导出)──
|
||||
var _tiles: Dictionary[String, Texture2D]:
|
||||
get:
|
||||
return _palette.tiles
|
||||
var _floor_tiles: Array[Texture2D]:
|
||||
get:
|
||||
return _palette.floor_tiles
|
||||
var _tiles_ok: bool:
|
||||
get:
|
||||
return _palette.tiles_ok
|
||||
var _seal_tex: Texture2D:
|
||||
get:
|
||||
return _palette.seal_tex
|
||||
var _tiles_board_type: String:
|
||||
get:
|
||||
return _palette.tiles_board_type
|
||||
var _digit_dark_ink: bool:
|
||||
get:
|
||||
return _palette.digit_dark_ink
|
||||
var _digit_plate: bool:
|
||||
get:
|
||||
return _palette.digit_plate
|
||||
var _hero_tex_idle: Texture2D:
|
||||
get:
|
||||
return _hero_sprite.tex_idle
|
||||
set(v):
|
||||
_hero_sprite.tex_idle = v
|
||||
var _hero_tex_walk: Texture2D:
|
||||
get:
|
||||
return _hero_sprite.tex_walk
|
||||
var _hero_last: Vector2i:
|
||||
get:
|
||||
return _hero_sprite.last
|
||||
set(v):
|
||||
_hero_sprite.last = v
|
||||
var _hero_move_tw: Tween:
|
||||
get:
|
||||
return _hero_sprite.move_tw
|
||||
var _hero_bump_tw: Tween:
|
||||
get:
|
||||
return _hero_sprite.bump_tw
|
||||
var _hero_dig_tw: Tween:
|
||||
get:
|
||||
return _hero_sprite.dig_tw
|
||||
|
||||
# ── 件成员同名转发(探针直读面零改动:读 _palette.seal_tex、读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)──
|
||||
# ---- 视野迷雾(纯呈现层;现行事实源=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)----
|
||||
const FOG_RADIUS := 8 # 菱形灯光区基准半径(曼哈顿距离 ≤R——与微光环「周围 8 格」同圈起步;半径基准值改动=非白名单须用户)
|
||||
var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面)
|
||||
@@ -161,8 +117,8 @@ func _init() -> void:
|
||||
_desat_mat = ShaderMaterial.new()
|
||||
_desat_mat.shader = load("res://presentation/views/desaturation.gdshader")
|
||||
_desat_mat.set_shader_parameter("strength", 0.0)
|
||||
_hero_tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE)
|
||||
_hero_tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK)
|
||||
_hero_sprite.tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE)
|
||||
_hero_sprite.tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK)
|
||||
_palette.load_board_tiles() # 高墙/地面瓦片整批守卫装载(缺位静默回退——英雄同款纪律,禁刷屏)
|
||||
|
||||
|
||||
@@ -247,8 +203,8 @@ func set_cell_area(avail: Vector2) -> void:
|
||||
var u: Node2D = _cells[c]
|
||||
u.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
if _world.hero != null and _hero_last.x >= 0:
|
||||
_place_hero(_hero_last, _hero_last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点)
|
||||
if _world.hero != null and _hero_sprite.last.x >= 0:
|
||||
_place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点)
|
||||
_refresh_slab()
|
||||
_update_world_size()
|
||||
|
||||
@@ -276,15 +232,15 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
_glow.clear()
|
||||
_glow_errors.clear()
|
||||
_contact_unit = null
|
||||
_hero_last = Vector2i(-99, -99)
|
||||
_hero_sprite.last = Vector2i(-99, -99)
|
||||
_r0.clear()
|
||||
_hover = Vector2i(-9, -9)
|
||||
for c in _board.compute_r0():
|
||||
_r0[c] = true
|
||||
# 瓦片套随板图型切换(board_gen 词表 v1.1 dungeon/underdark/deep_underdark;
|
||||
# 同图型重复 rebuild 零重载,_tiles_board_type 守卫)
|
||||
# 同图型重复 rebuild 零重载,_palette.tiles_board_type 守卫)
|
||||
var bt: String = _board.board_type if _board.board_type != "" else "dungeon"
|
||||
if bt != _tiles_board_type:
|
||||
if bt != _palette.tiles_board_type:
|
||||
_palette.load_board_tiles(bt)
|
||||
var stride := _cell + float(SEP)
|
||||
for y in _board.height:
|
||||
@@ -296,7 +252,7 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
u.position = Vector2(c) * stride
|
||||
u.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
|
||||
if _tiles_ok:
|
||||
if _palette.tiles_ok:
|
||||
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
|
||||
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
|
||||
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
|
||||
@@ -629,7 +585,7 @@ func _badge_glyph(category: String) -> String:
|
||||
#(dungeon 瓦片实测达标维持);深墨族 1-3 青墨/4-5 赭墨/6-8 锈墨(色温语义同构——underdark/
|
||||
# deep_underdark 亮白筋纹瓦片上亮墨对亮斑 CR 1.0–1.9 融字,深墨族 5.7–11.1;选族判据见 TilePalette.digit_ink_mode)
|
||||
func _digit_ink(d: int) -> Color:
|
||||
if _digit_dark_ink:
|
||||
if _palette.digit_dark_ink:
|
||||
if d <= 3:
|
||||
return UiKit.INK_COLD
|
||||
if d <= 5:
|
||||
@@ -655,7 +611,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var digit_col := COL_DIGIT
|
||||
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
|
||||
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
|
||||
u.tex_seal = _seal_tex if (seal and not revealed and _seal_tex != null) else null
|
||||
u.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
if removed:
|
||||
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
|
||||
seal = false
|
||||
@@ -678,7 +634,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
else:
|
||||
if seal:
|
||||
bg = COL_BRICK
|
||||
if _seal_tex == null:
|
||||
if _palette.seal_tex == null:
|
||||
glyph = "封"
|
||||
digit_col = COL_SEAL
|
||||
else:
|
||||
@@ -692,7 +648,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
#(绘制路径零像素变化),仅英雄格保留蓝框载体。
|
||||
var sb: StyleBoxFlat = null
|
||||
var tile: Texture2D = null
|
||||
if _tiles_ok:
|
||||
if _palette.tiles_ok:
|
||||
tile = _floor_tile_for(c) if revealed else _wall_tile_for(c)
|
||||
else:
|
||||
# 素材整批缺位回退 = 现行程序化平色路径(fresh checkout 冒烟不炸——需求卡片 §2.2)
|
||||
@@ -702,7 +658,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT,UiKit.RADIUS_OBJECT, 0)
|
||||
sb.set_border_width_all(3)
|
||||
sb.border_color = COL_HERO
|
||||
if glyph == "" and _hero_tex_idle == null:
|
||||
if glyph == "" and _hero_sprite.tex_idle == null:
|
||||
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
|
||||
glyph = "人"
|
||||
digit_col = COL_HERO
|
||||
@@ -711,8 +667,8 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
u.glyph = glyph
|
||||
u.glyph_color = digit_col
|
||||
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
|
||||
#(_digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
|
||||
var plate_digit := _digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人"
|
||||
#(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
|
||||
var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人"
|
||||
if plate_digit:
|
||||
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
|
||||
u.glyph_color = digit_col
|
||||
@@ -739,7 +695,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3))
|
||||
if code == 1 and u.facade_south:
|
||||
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
|
||||
if code == 0 and _tiles_ok:
|
||||
if code == 0 and _palette.tiles_ok:
|
||||
continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边)
|
||||
u.outline_sides.append(code)
|
||||
else:
|
||||
@@ -748,9 +704,9 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
u.proj_band = u.edge * WALL_PROJECTION_FRAC
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30%
|
||||
# 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义
|
||||
if south_in and not _minesweeper.is_revealed(south_n) and _tiles_ok:
|
||||
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
|
||||
u.facade_strip = true
|
||||
u.tex_facade = _tiles.get("wall_center")
|
||||
u.tex_facade = _palette.tiles.get("wall_center")
|
||||
# ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有
|
||||
# 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算)
|
||||
if not revealed:
|
||||
|
||||
@@ -422,7 +422,7 @@ func _ready() -> void:
|
||||
# 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/
|
||||
# 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕
|
||||
# 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。
|
||||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure._node_curio_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
|
||||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
|
||||
# 停点态判据 = _node_curio_pending(奖励节点三选一停点住此——原条件查
|
||||
# _curio_pending〔关卡完成三选一态〕与奖励头部互斥、恒不可命中=拦截能力未证病根,
|
||||
# 卡⑤ d4 转正随修;头部校验串本体零触碰)
|
||||
@@ -440,10 +440,10 @@ func _ready() -> void:
|
||||
_problems.append("三选一停点打开时输入未锁定")
|
||||
if main.curio_overlay._continue_btn.visible:
|
||||
main.curio_overlay.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure._node_curio_pending.is_empty():
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
main.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
|
||||
main.curio_overlay.press_confirm()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty():
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.curio_overlay.press_card(0)
|
||||
main.curio_overlay.press_confirm()
|
||||
else:
|
||||
@@ -610,7 +610,7 @@ func _ready() -> void:
|
||||
elif main.curio_overlay.visible:
|
||||
if main.curio_overlay._continue_btn.visible:
|
||||
main.curio_overlay.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty():
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.curio_overlay.press_card(0)
|
||||
main.curio_overlay.press_confirm()
|
||||
else:
|
||||
@@ -719,7 +719,7 @@ func _ready() -> void:
|
||||
while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||||
ol_r_wait += 1
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.runner == null or int(main.run_scope.runner.run_ledger.bonus.get("atk", 0)) != 5:
|
||||
if main.run_scope.runner == null or main.run_scope.runner.journal().bonus_total("atk") != 5:
|
||||
_problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)")
|
||||
if ol_r_wait >= 24:
|
||||
_problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)")
|
||||
|
||||
@@ -265,8 +265,8 @@ func _ready() -> void:
|
||||
# `u.tex_seal = null` 会覆盖条件赋值=素材在位时封印不可见(headless 缺盘走字形回退
|
||||
# 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ----
|
||||
var seal_unit = bv.cells_probe().get(seal_t)
|
||||
if bv._seal_tex != null:
|
||||
if seal_unit == null or seal_unit.tex_seal != bv._seal_tex:
|
||||
if bv._palette.seal_tex != null:
|
||||
if seal_unit == null or seal_unit.tex_seal != bv._palette.seal_tex:
|
||||
_problems.append("case④b 封印纹理槽未挂封印印记纹理(tex_seal=%s)" % ["无单元" if seal_unit == null else str(seal_unit.tex_seal)])
|
||||
elif seal_unit == null or seal_unit.glyph != "封":
|
||||
_problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if seal_unit == null else String(seal_unit.glyph)])
|
||||
@@ -332,7 +332,7 @@ func _ready() -> void:
|
||||
var sp9: Sprite2D = bv._world.hero
|
||||
if not sp9.flip_h:
|
||||
_problems.append("case⑨ 右移后未翻转朝向(flip_h=false,素材基准朝左)")
|
||||
if sp9.texture != bv._hero_tex_idle:
|
||||
if sp9.texture != bv._hero_sprite.tex_idle:
|
||||
_problems.append("case⑨ 到位未回站帧(texture=%s)" % str(sp9.texture))
|
||||
if sp9.scale != Vector2.ONE * bv.HERO_SPRITE_SCALE:
|
||||
_problems.append("case⑨ 缩放档漂移(%s 应=%s 均匀)" % [str(sp9.scale), str(bv.HERO_SPRITE_SCALE)])
|
||||
@@ -355,8 +355,8 @@ func _ready() -> void:
|
||||
if bv._world.hero != null:
|
||||
var hp11: Vector2i = ms.hero_pos()
|
||||
var u11 = bv.cells_probe()[hp11]
|
||||
var tex_saved: Texture2D = bv._hero_tex_idle
|
||||
bv._hero_tex_idle = null
|
||||
var tex_saved: Texture2D = bv._hero_sprite.tex_idle
|
||||
bv._hero_sprite.tex_idle = null
|
||||
bv.refresh()
|
||||
var digit11: int = int(ms.reveal_record(hp11).get("digit", 0))
|
||||
if digit11 == 0:
|
||||
@@ -364,7 +364,7 @@ func _ready() -> void:
|
||||
_problems.append("case⑪ 贴图缺位未回退字形「人」(glyph=%s)" % u11.glyph)
|
||||
elif u11.glyph != str(digit11):
|
||||
_problems.append("case⑪ 缺位态数字格字形被破坏(glyph=%s 应=%d)" % [u11.glyph, digit11])
|
||||
bv._hero_tex_idle = tex_saved
|
||||
bv._hero_sprite.tex_idle = tex_saved
|
||||
bv.refresh()
|
||||
# ---- case⑭ 变异角标(flat_damage 未开格视觉标记;resolver 注入面+落位+复位) ----
|
||||
var mark_t: Vector2i = _pick_forbidden(bv, ms) # 任一未开格(禁入=未开非相邻)
|
||||
|
||||
@@ -144,8 +144,8 @@ func _ready() -> void:
|
||||
problems.append("当局数据 HP 失对(%s)" % str(rd.get("hp")))
|
||||
if int(rd.get("gp", -1)) != int(r.gp):
|
||||
problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)])
|
||||
if int(rd.get("keystone", -1)) != int(main.run_scope.adventure.keystone):
|
||||
problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.keystone)])
|
||||
if int(rd.get("keystone", -1)) != main.run_scope.adventure.journal().keystone_gained():
|
||||
problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.journal().keystone_gained())])
|
||||
var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix")
|
||||
if close_btn == null:
|
||||
problems.append("浮窗关闭钮缺失(真实收窗路径中断)")
|
||||
|
||||
+30
-15
@@ -140,13 +140,14 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
|
||||
"level_maps": [], # 逐关节点图标识(生成图 = 关卡序号+本局主种子——MC 分层出数)
|
||||
}
|
||||
adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows))
|
||||
adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
|
||||
adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger))
|
||||
# 信号直连(拆件批 T3):runner 信号在 level_started 重取点重连(转发族退役——重取点前移到本回调)
|
||||
adr.level_started.connect(_on_autoplay_level_started.bind(adr, st, collect_rows, logger))
|
||||
# 首关信号在 start_adventure 内同步发射(驱动循环起步前)——入口候选订阅须在本行前就位:
|
||||
# _on_autoplay_level_started 于本行同步执行(start_adventure 返回前),runner 三信号先于驱动循环就绪
|
||||
# 三选一停点驱动(确定性策略候选取首件、
|
||||
# 兑付后推进下一关——零随机流自开,同种子逐位可复放;无满位替换策略)
|
||||
adr.curio_choice_presented.connect(_on_adventure_bonus.bind(st, adr))
|
||||
adr.curio_resolved.connect(_on_adventure_curio_resolved.bind(st, adr))
|
||||
# 兑付后推进下一关——零随机流自开,同种子逐位可复放;无满位替换策略;信号面归停点件)
|
||||
adr.stop().curio_choice_presented.connect(_on_adventure_bonus.bind(st, adr))
|
||||
adr.stop().curio_resolved.connect(_on_adventure_curio_resolved.bind(st, adr))
|
||||
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
|
||||
adr.set_hero_override(atk_pct, hp_pct)
|
||||
# 同调分层注入(缺省 {} 与既有调用方逐位一致零行为变更)
|
||||
@@ -190,6 +191,20 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
return st
|
||||
|
||||
|
||||
# 每关开始(autoplay 直连面):重取 runner 并重连三信号(恰好一次——同关守卫:
|
||||
# level_started 每关恰一次发射,bind Callable 每次新建故以关卡号守卫防重入)
|
||||
static func _on_autoplay_level_started(level_index: int, _display_name: String, adr, st: Dictionary, collect_rows: bool, logger) -> void:
|
||||
if int(st.get("wired_level", -1)) == int(level_index):
|
||||
return # 同关重入防御(恰好一次——bind 句柄每回调新建,不以 Callable 相等判重)
|
||||
st["wired_level"] = int(level_index)
|
||||
var r = adr.runner()
|
||||
if r == null:
|
||||
return
|
||||
r.step_row.connect(_on_step_row.bind(adr, st, collect_rows))
|
||||
r.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
|
||||
r.wallet_changed.connect(_on_adventure_wallet.bind(st, logger))
|
||||
|
||||
|
||||
# 冒险模式节点采集(_on_node_entered 的 AdventureRunner 门面版——runner 逐关更替,经 adr.runner() 重取;
|
||||
# encounter_max_hp_seq 抓 effective_max_hp:node_entered 先于 begin_battle,encounter 尚为空字典,
|
||||
# 该值 = begin_battle 将收到 hero_max_hp 同一表达式〔规划书抓取点与代码事实冲突,按代码为准已登记〕)
|
||||
@@ -209,26 +224,26 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
logger.attach_battle(r.current_battle())
|
||||
if t == "boss":
|
||||
st.boss_depth = int(node.get("depth", -1))
|
||||
st.hp_at_boss = int(adr.ledger["hero_hp"])
|
||||
st.hp_at_boss = adr.stats().hero_hp
|
||||
elif t == "reward":
|
||||
# 奖励节点三选一(共享池确定性策略;TP0 起 316-f 判读:1 级最稀有、
|
||||
# 同档并列取最左;替换放弃新件;兑付后 continue_after_node 回图选路)
|
||||
var ndraw: Dictionary = adr.node_curio_candidates()
|
||||
var ndraw: Dictionary = adr.stop().node_curio_candidates()
|
||||
var ncands: Array = ndraw.get("candidates", []) as Array
|
||||
st.reward_events.append({"node_curio_pick": ncands.duplicate(true)})
|
||||
if ncands.is_empty():
|
||||
r.continue_after_node() # 牌库空合法态直通(池耗即枯竭——防御保留)
|
||||
else:
|
||||
adr.present_node_curio(ncands)
|
||||
adr.stop().present_node_curio(ncands)
|
||||
var npick: Dictionary = _pick_token(ncands, String(st.get("feat_mode", "off")))
|
||||
if adr.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))):
|
||||
if adr.stop().apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))):
|
||||
# 专长凭证拿到即开——候选已被替换为专长三选一,就地再选一轮(确定性策略同款 316-f);
|
||||
# 不作此分支则节点停点悬空→选路卡死(MC timeout 形态)
|
||||
if String(npick.get("kind", "")) == "feat_ticket":
|
||||
var fcands: Array = (adr.node_curio_pending_state().get("candidates", []) as Array)
|
||||
var fcands: Array = (adr.stop().node_curio_pending_state().get("candidates", []) as Array)
|
||||
if not fcands.is_empty():
|
||||
var fpick: Dictionary = _pick_token(fcands, String(st.get("feat_mode", "off")))
|
||||
adr.apply_node_curio(String(fpick.get("kind", "")), String(fpick.get("tier", "")), String(fpick.get("feat_id", "")))
|
||||
adr.stop().apply_node_curio(String(fpick.get("kind", "")), String(fpick.get("tier", "")), String(fpick.get("feat_id", "")))
|
||||
else:
|
||||
st.result = "advance_failed"
|
||||
elif t == "rest":
|
||||
@@ -259,14 +274,14 @@ static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary,
|
||||
# 奖励事件随 reward_events 留档)
|
||||
static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void:
|
||||
var pl := payload.duplicate(true)
|
||||
pl["bonus_max_hp"] = int(adr.run_ledger.bonus.get("max_hp", 0)) # 观测面:停点时刻加算区快照(回血精确断言用)
|
||||
pl["bonus_max_hp"] = adr.journal().bonus_total("max_hp") # 观测面:停点时刻加算区快照(回血精确断言用)
|
||||
st.reward_events.append({"curio_pick": pl})
|
||||
var candidates: Array = payload.get("candidates", []) as Array
|
||||
if candidates.is_empty():
|
||||
st.result = "advance_failed"
|
||||
return
|
||||
var pick: Dictionary = _pick_token(candidates, String(st.get("feat_mode", "off")))
|
||||
if not adr.pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))):
|
||||
if not adr.stop().pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))):
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
@@ -557,7 +572,7 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
|
||||
# ①标雷误挖:把「封印已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错,
|
||||
# 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × NORMAL_MINE_SLIP_P(目标完美+2/板)
|
||||
# ②安全挖跳过:把「挖已证安全格」动作转成本轮停手(少挖几个正确的雷——信息与推进双损失)
|
||||
# ③卡死赌挖:完美档无已证动作时刷新/停手拖轮次吃敌方垫刀(实证=完美档 41% 通关全败于慢玩
|
||||
# ③卡死赌挖:完美档无已证动作时刷新/停手拖轮次吃敌方垫刀(实证=完美档 41% 冒险成功;全败于慢玩
|
||||
# 垫刀非踩雷)——真人会按概率赌一个边界格(用户实证口径「有些地方因为我看不到数字,也是盲挖
|
||||
# 的」);普通档不刷新(用户自报全程没用刷新),赌挖替代刷新/停手
|
||||
# 失误流 = st.prng(策略流 RNG,种子派生自局种子——同种子逐位可回放;核心七流零消费零扰动)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿;2026-10-27 用户裁「标准是尺、不以现状为尺」:09 存量红照实记(exit=1+现行红项指纹,两条恶化各自点名可核),gdstyle 基线登记值回退 1302/21(波 0 的 18 条陈旧键清理保留),gdstyle_gate 逐条违规行加 FAIL 前缀并加「refresh 禁抬高登记值」守卫——存量红期间任何新增违规或进一步恶化都会改指纹被拦;阈值口径不变=gdstyle 默认值(文件 1000 行/类变量 15 等),还债归迭代 40 卡①(battle_orchestrator 1314→≤1000)与迭代 41 卡③(level_map_screen 22→≤15),还清后重新生成基线归绿",
|
||||
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿;2026-10-27 用户裁「标准是尺、不以现状为尺」:09 存量红照实记(exit=1+现行红项指纹,两条恶化各自点名可核),gdstyle 基线登记值回退 1302/21(波 0 的 18 条陈旧键清理保留),gdstyle_gate 逐条违规行加 FAIL 前缀并加「refresh 禁抬高登记值」守卫——存量红期间任何新增违规或进一步恶化都会改指纹被拦;阈值口径不变=gdstyle 默认值(文件 1000 行/类变量 15 等),还债归迭代 40 卡①(battle_orchestrator 1314→≤1000)与迭代 41 卡③(level_map_screen 22→≤15),还清后重新生成基线归绿;2026-10-27 用户特批带红合并(迭代39 拆件批):02 记存量红(fp f74b9a48ef4a=round_flow EV 锚 goblin 0.9148∉[1.00,1.20],触发器批有意行为改动,旧带宽锚触发器前设计,重锚归后续数值批待用户发起);08d 记存量红(red-nokeys=生涯.md:55 引 adventure_runner.gd:814 行号漂移,B1 拆分后 660 行,另 :50/:60 与 元进度.md:70 建议级,修复归合并后文档批,反哺点在册);07 doc_link 基线红→现绿改善随下次基线重生成记账;09 指纹经 S9 注解修复(f96632de)回归登记值 91d45583cf6d",
|
||||
"gates": {
|
||||
"00 冒烟测试": {
|
||||
"exit": "0",
|
||||
@@ -10,8 +10,8 @@
|
||||
"fp": "green"
|
||||
},
|
||||
"02 单元与集成测试": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
"exit": "1",
|
||||
"fp": "f74b9a48ef4a"
|
||||
},
|
||||
"03 探针 --click": {
|
||||
"exit": "0",
|
||||
@@ -58,8 +58,8 @@
|
||||
"fp": "skip"
|
||||
},
|
||||
"08d 引用可核": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
"exit": "1",
|
||||
"fp": "red-nokeys"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 断点基线对拍采集脚本(迭代 39 拆件批 T0 载体,批末随中间产物退役——非长期测试件)。
|
||||
# 固定种子集驱动完整冒险至各停点(节点入口/选路/三选一在途/兑付后待推进),逐停点
|
||||
# dump 断点 Dictionary 全键值(直调 _bp_parts 写侧取值面)→ JSON 落盘,供改前/改后
|
||||
# 逐键逐位比对(bp1:同种子改前改后 27 键逐位一致)。
|
||||
# 运行:BP_DUMP_OUT=<json 路径> godot --headless --path . --script res://tests/bp_baseline_dump.gd
|
||||
# 缺省输出 user://bp_baseline_dump.json。
|
||||
# 驱动与 sim/level_autoplay 同构(完美档战斗驱动 + 确定性选件/选路)——零随机流自开,
|
||||
# 同种子逐位可复放;dump 时点由驱动循环状态判定,确定性时点。
|
||||
extends SceneTree
|
||||
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const Autoplay := preload("res://sim/level_autoplay.gd")
|
||||
|
||||
const SEEDS: Array[int] = [1234, 4321, 910042, 20260101, 20260102, 20260103]
|
||||
const PARAMS := {
|
||||
"hero_override": {},
|
||||
"treasure_unlocked": {"wide_scout_band": true},
|
||||
"attunement": ["sky_eye", "incense_stick", "refresh_practice"], # 充能账非零形态+刷新存量加仓(attunement_charges/refresh_stock 键覆盖)
|
||||
"attune_refresh_y": 2, # Y 池非零形态(attune_refresh_y 键覆盖)
|
||||
"shop_mode": "pool", # 共享池货架面(shop_shelf/shop_visit_buys/shop_purchased 覆盖)
|
||||
"relics_seed": ["twin_pick"], # 特性持有集+adventure_counters 非空形态(adv_twin_digs 计数器用户)
|
||||
}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[bp_baseline_dump] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
var runs: Array = []
|
||||
for seed_val in SEEDS:
|
||||
var run := _drive_dump_run(db, int(seed_val))
|
||||
runs.append({"seed": int(seed_val), "result": String(run["result"]), "dumps": run["dumps"]})
|
||||
print("[bp_baseline_dump] seed=%d result=%s dumps=%d" % [int(seed_val), String(run["result"]), (run["dumps"] as Array).size()])
|
||||
var out_path := OS.get_environment("BP_DUMP_OUT")
|
||||
if out_path.is_empty():
|
||||
out_path = "user://bp_baseline_dump.json"
|
||||
var f := FileAccess.open(out_path, FileAccess.WRITE)
|
||||
if f == null:
|
||||
print("[bp_baseline_dump] FAIL — 输出文件打不开: %s" % out_path)
|
||||
quit(1)
|
||||
return
|
||||
f.store_string(JSON.stringify({"generated_by": "bp_baseline_dump", "seeds": SEEDS, "runs": runs}, " "))
|
||||
f.close()
|
||||
print("[bp_baseline_dump] DONE — %s" % out_path)
|
||||
quit(0)
|
||||
|
||||
|
||||
# 单局驱动+停点 dump(返回 {result, dumps};dumps 元素 = {seq, tag, level_index, node_id, bp})
|
||||
func _drive_dump_run(db, seed_val: int) -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var adr = AdventureRunnerScript.new(db, rng)
|
||||
var ctx := {"result": "", "dumps": [] as Array, "seq": 0, "adr": adr, "db": db}
|
||||
# 信号直连(拆件批 T3——转发族退役):runner 信号在 level_started 重取点重连(同关守卫恰一次);
|
||||
# 停点件信号面订 stop()(每冒险一次)
|
||||
adr.level_started.connect(_on_dump_level_started.bind(ctx))
|
||||
adr.stop().curio_resolved.connect(_on_curio_resolved.bind(ctx))
|
||||
if not adr.start_adventure(seed_val, PARAMS):
|
||||
ctx["result"] = "start_failed"
|
||||
return ctx
|
||||
var guard := 2000
|
||||
while String(ctx["result"]) == "" and guard > 0:
|
||||
guard -= 1
|
||||
if not (adr.stop()._node_pending as Dictionary).is_empty():
|
||||
_dump(ctx, "node_curio_pending")
|
||||
var ncands: Array = (adr.stop()._node_pending.get("candidates", []) as Array)
|
||||
var npick: Dictionary = Autoplay._pick_token(ncands, "off")
|
||||
if not adr.stop().apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))):
|
||||
ctx["result"] = "node_advance_failed"
|
||||
continue
|
||||
if not (adr.stop()._pending as Dictionary).is_empty():
|
||||
_dump(ctx, "curio_pending")
|
||||
var cands: Array = (adr.stop()._pending.get("candidates", []) as Array)
|
||||
var pick: Dictionary = Autoplay._pick_token(cands, "off")
|
||||
if not adr.stop().pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))):
|
||||
ctx["result"] = "advance_failed"
|
||||
continue
|
||||
if bool(adr._pending_next_level):
|
||||
_dump(ctx, "resolved_wait_continue")
|
||||
if not adr.continue_to_next_level():
|
||||
ctx["result"] = "advance_failed"
|
||||
continue
|
||||
var runner = adr.runner()
|
||||
if runner == null:
|
||||
break # 冒险已收束或停点态未覆盖(防御)
|
||||
if runner.awaiting_choice():
|
||||
_dump(ctx, "route_choice")
|
||||
var ids: Array = runner.next_candidate_ids()
|
||||
if ids.is_empty():
|
||||
ctx["result"] = "no_candidates"
|
||||
break
|
||||
if not runner.pick_next_node(int(ids[0])):
|
||||
ctx["result"] = "route_failed"
|
||||
continue
|
||||
var settlement = runner.current_battle()
|
||||
if settlement != null and String(settlement.battle_result) == "":
|
||||
_drive_battle(ctx, runner, settlement)
|
||||
continue
|
||||
# 收束态分派:结算受理后(state=idle/settling)按 summary 定局;其余态无战无停点无选路 = 死循环防护
|
||||
if String(ctx["result"]) == "":
|
||||
var sum: Dictionary = adr.summary if adr.summary != null else {}
|
||||
if not sum.is_empty():
|
||||
ctx["result"] = String(sum.get("result", "settled"))
|
||||
break
|
||||
if String(adr.state) in ["idle", "settling"]:
|
||||
ctx["result"] = "settled_no_summary"
|
||||
break
|
||||
if guard == 1:
|
||||
ctx["result"] = "timeout"
|
||||
return ctx
|
||||
|
||||
|
||||
# 完美档战斗驱动(复用 sim 同款决策——上界策略;st 键集满足 _autoplay_battle 消费面)
|
||||
func _drive_battle(ctx: Dictionary, runner, settlement) -> void:
|
||||
var st := {"result": "", "rounds": 0, "refreshes": 0, "seal_placed": 0,
|
||||
"policy": "perfect", "adv": false}
|
||||
Autoplay._autoplay_battle(runner, settlement, st, null)
|
||||
|
||||
|
||||
# 停点 dump(写侧取值面直调;_runner 为 null 的停点态节点域字段按缺省落档——与真机同构)
|
||||
static func _dump(ctx: Dictionary, tag: String) -> void:
|
||||
var adr = ctx["adr"]
|
||||
var bp: Dictionary = adr._bp_parts()
|
||||
var runner = adr.runner()
|
||||
var dumps: Array = ctx["dumps"]
|
||||
ctx["seq"] = int(ctx["seq"]) + 1
|
||||
dumps.append({
|
||||
"seq": int(ctx["seq"]),
|
||||
"tag": String(tag),
|
||||
"level_index": int(adr.level_index),
|
||||
"node_id": int(runner.current_node_id()) if runner != null else -1,
|
||||
"bp": bp,
|
||||
})
|
||||
|
||||
|
||||
# 每关开始(dump 直连面):重取 runner 重连 node_entered(同关守卫——level_started 每关恰一次发射)
|
||||
static func _on_dump_level_started(_li: int, _dn: String, ctx: Dictionary) -> void:
|
||||
var adr = ctx["adr"]
|
||||
if String(ctx.get("wired_level", "")) == "%s|%d" % [str(adr.master_seed), int(adr.level_index)]:
|
||||
return
|
||||
ctx["wired_level"] = "%s|%d" % [str(adr.master_seed), int(adr.level_index)]
|
||||
var r = adr.runner()
|
||||
if r == null:
|
||||
return
|
||||
r.node_entered.connect(_on_node_entered.bind(ctx))
|
||||
|
||||
|
||||
# 节点入口驱动(信号时点):入口 dump 覆盖节点域字段形态;rest/shop 直通继续,reward 呈现三选一
|
||||
static func _on_node_entered(node: Dictionary, _info: Dictionary, ctx: Dictionary) -> void:
|
||||
var adr = ctx["adr"]
|
||||
var runner = adr.runner()
|
||||
if runner == null:
|
||||
return
|
||||
var t := String(node.get("type", ""))
|
||||
_dump(ctx, "node_" + t)
|
||||
if t == "rest" or t == "shop":
|
||||
runner.continue_after_node()
|
||||
elif t == "reward":
|
||||
var ndraw: Dictionary = adr.stop().node_curio_candidates()
|
||||
var ncands: Array = ndraw.get("candidates", []) as Array
|
||||
if ncands.is_empty():
|
||||
runner.continue_after_node() # 牌库空合法态直通(池耗即枯竭)
|
||||
else:
|
||||
adr.stop().present_node_curio(ncands)
|
||||
|
||||
|
||||
# 节点三选一兑付完成回图(context=reward_node → continue_after_node 回选路)
|
||||
static func _on_curio_resolved(result: Dictionary, ctx: Dictionary) -> void:
|
||||
if String(result.get("context", "")) != "reward_node":
|
||||
return
|
||||
var adr = ctx["adr"]
|
||||
var runner = adr.runner()
|
||||
if runner != null and not runner.continue_after_node():
|
||||
ctx["result"] = "node_advance_failed"
|
||||
@@ -0,0 +1 @@
|
||||
uid://byleq6f7m8qm6
|
||||
@@ -77,8 +77,8 @@ func _feats_breakpoint_roundtrip() -> void:
|
||||
_db.feats = rt
|
||||
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
|
||||
a_r.start_adventure(72) # 源 :723(受理断言留单元宿主)
|
||||
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
a_r.pick_curio("relic", "rare", "x3") # 源 :781(兑付断言留单元宿主)
|
||||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
a_r.stop().pick_curio("relic", "rare", "x3") # 源 :781(兑付断言留单元宿主)
|
||||
# 断点往返(持有集+余库 feat_id 随行)
|
||||
var bp_path := "user://_smoke_feats_bp.json" # 段首清场(存档域隔离纪律——上轮残留断点先删)
|
||||
if FileAccess.file_exists(bp_path):
|
||||
@@ -86,7 +86,7 @@ func _feats_breakpoint_roundtrip() -> void:
|
||||
var sm_r = load("res://core/save_manager.gd").new(bp_path)
|
||||
sm_r.load_from_disk()
|
||||
a_r.set_save_manager(sm_r)
|
||||
a_r._on_level_finished({"result": "victory", "hp": int(a_r.ledger.get("hero_hp", 26)), "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
a_r._on_level_finished({"result": "victory", "hp": int(a_r.stats().hero_hp), "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var bp_r: Dictionary = sm_r.breakpoint_snapshot()
|
||||
Kit.check("断点附随件:relics 持有集入快照(迭代 12 T1 扩列)", (bp_r.get("relics", []) as Array).has("x3"))
|
||||
Kit.check("断点附随件:adventure 计数器账本键在案(T6/T7 扩列)", bp_r.has("adventure_counters"))
|
||||
|
||||
@@ -56,8 +56,9 @@ func _smoke_generated_map_determinism() -> void:
|
||||
var rng_c = _fresh_rng(910005)
|
||||
var counter_before: int = rng_c.counter()
|
||||
var runner2 = load("res://core/level_runner.gd").new(_db, rng_c)
|
||||
runner2.hero_hp = 20 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
|
||||
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
|
||||
{"hero_hp": 20, "gp": 0}, {}, Callable(self, "_smoke_rest_stub"), {}, true))
|
||||
Callable(self, "_smoke_rest_stub"), true))
|
||||
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
|
||||
var params3: Dictionary = _db.level_gen_params_row(3)
|
||||
var entry_count: int = int(params3.get("entry_count", -1))
|
||||
|
||||
@@ -57,6 +57,8 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/node_map_growth_test.gd", "expect": 0}, # 生成图生长递增+参数无解 fail-fast 子进程通道(卡① V8/V4)
|
||||
{"path": "res://tests/unit/core/level_table_validate_test.gd", "expect": 0}, # 消费侧加载校验四检+良构基底正负向(卡② T1/V1〔v1〕)
|
||||
{"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点
|
||||
{"path": "res://tests/unit/core/hero_ledger_test.gd", "expect": 0}, # 主角账公式逐值/三通道日记/断点片段往返与旧档回退(拆件批 V1:hr1/hr1-s/hr2)
|
||||
{"path": "res://tests/unit/battle_settlement/trigger_cond_test.gd", "expect": 0}, # 触发条件机制 V1/V2/V4/V6/V7/V8/V9/V10(触发器批——出域拒绝/双面过滤/闩语义/事实面/池纪律/两示范/零扰动)
|
||||
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
|
||||
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
|
||||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||||
|
||||
@@ -493,7 +493,7 @@ func _smoke_skill_pool() -> void:
|
||||
|
||||
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
|
||||
func _smoke_skill_pool_data() -> void:
|
||||
Kit.check("技能表十四行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕)", _db.skill.rows.size() == 14)
|
||||
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.skill.rows.size() == 16)
|
||||
var basic: Dictionary = _db.skill.row_by_id("attack")
|
||||
Kit.check("普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)",
|
||||
not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
|
||||
@@ -560,8 +560,8 @@ func _smoke_skill_pool_data() -> void:
|
||||
# 行集键 = (level_index, row_kind, depth_band, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池,
|
||||
# enemy_id 不再唯一;同带混编多行时每行各一池(行序 = 文件序 = 契约序)
|
||||
var expect_pools := {
|
||||
"1_battle_0_goblin": {"attack": 9, "lunge": 1},
|
||||
"1_battle_1_goblin": {"attack": 9, "lunge": 1},
|
||||
"1_battle_0_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
|
||||
"1_battle_1_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
|
||||
"1_battle_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_battle_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_boss_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
|
||||
@@ -574,10 +574,10 @@ func _smoke_skill_pool_data() -> void:
|
||||
"2_battle_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_boss_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
|
||||
"3_battle_0_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_battle_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_battle_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_battle_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1, "sneak_ambush": 2},
|
||||
"3_battle_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1, "sneak_ambush": 2},
|
||||
"3_battle_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1, "sneak_ambush": 2},
|
||||
"3_battle_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_boss_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
# 迭代 35 生成图深度扩面覆盖批八行=同关同 row_kind 顶档行的池克隆(零新池、零新技能引用):
|
||||
@@ -602,7 +602,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
got[String(entry.skill_id)] = int(entry.weight)
|
||||
if got != expect_pools[rk]:
|
||||
pools_ok = false
|
||||
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
|
||||
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
|
||||
var basic_count_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var n := 0
|
||||
@@ -630,6 +630,17 @@ func _smoke_skill_pool_data() -> void:
|
||||
if n_buff > 1 or n_debuff > 1:
|
||||
comp_ok = false
|
||||
Kit.check("逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)", comp_ok)
|
||||
# 池构成纪律复查(触发条件批 V7 冒烟侧):每池至少一行无条件特殊技(普攻不计;trigger 缺省行计数)
|
||||
var uncond_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var n_uncond := 0
|
||||
for entry in row.pool:
|
||||
var srow_u: Dictionary = _db.skill_row(String(entry.skill_id))
|
||||
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
|
||||
n_uncond += 1
|
||||
if n_uncond < 1:
|
||||
uncond_ok = false
|
||||
Kit.check("每池至少一行无条件特殊技(触发条件批池构成纪律——逐池复查)〔触发器批 V7〕", uncond_ok)
|
||||
# Boss 半血解锁技·编排集成断言(真实编排回路:半血 → 下轮开轮闩检查 → 解锁行恰一次)
|
||||
var st_unlock: Dictionary = {"rows": []}
|
||||
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
|
||||
@@ -965,34 +976,35 @@ func _smoke_debuff_rider() -> void:
|
||||
# 引擎直构 + 编排授予段直注(_grant_seg._judge_seal=结算授予段封印判定单口))----
|
||||
func _smoke_counter_buckets() -> void:
|
||||
print("--- 效果引擎无状态化四桶正向验收(卡② §9 V1/V6)---")
|
||||
# V1——adventure 桶跨战持久:同一 RunLedger 下两战各真实 request_dig 一次,
|
||||
# V1——adventure 桶跨战持久:同一宿主字典下两战各真实 request_dig 一次,
|
||||
# 断付费计数经 ctx 就地写入宿主字典并跨战累积(副本搬运态:读不到上一战值→第 2 战仍 1,此断言会红)。
|
||||
var led = load("res://core/run_ledger.gd").new()
|
||||
var led: Dictionary[Variant, Variant] = {} # 冒险桶宿主字典(拆件批——桶归宿主)
|
||||
var RigS = load("res://core/battle_orchestrator.gd")
|
||||
var RngS = load("res://core/rng_service.gd")
|
||||
var enc1 = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
|
||||
"feats": ["twin_pick"], "tree_rules": [], "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "ledger": led}
|
||||
"node_type": "battle", "adv_bucket": led}
|
||||
var o1 = RigS.new(_db, _fresh_rng(779301))
|
||||
o1.begin_battle(enc1)
|
||||
var dig1: bool = o1.action_economy.request_dig(0, false)
|
||||
Kit.check("V1 第1战真实 request_dig 受理(begin_battle 后 ACTIVE 相位)〔无状态化 V1〕", dig1)
|
||||
Kit.check("V1 第1战付费挖格经 ctx 就地写 adventure 桶=1(无 export 搬运时点)〔无状态化 V1〕", int(led.adventure_counters.get("adv_twin_digs", 0)) == 1)
|
||||
Kit.check("V1 第1战付费挖格经 ctx 就地写 adventure 桶=1(无 export 搬运时点)〔无状态化 V1〕", int(led.get("adv_twin_digs", 0)) == 1)
|
||||
var enc2 = enc1.duplicate(true)
|
||||
enc2["battle_index"] = 2
|
||||
enc2["adv_bucket"] = led # 冒险桶宿主字典引用重挂(duplicate(true) 深拷贝值字典——单源化后跨战共享靠同引用)
|
||||
var o2 = RigS.new(_db, _fresh_rng(779302))
|
||||
o2.begin_battle(enc2)
|
||||
Kit.check("V1 第2战 begin_battle 经 ctx 读到上一战持久值=1(读路径走宿主字典引用,非副本种子回灌)〔无状态化 V1〕", EffectEngine.counter(o2.fx_host().counters_ctx, "adv_twin_digs") == 1)
|
||||
o2.action_economy.request_dig(0, false)
|
||||
Kit.check("V1 第2战挖后 adventure 桶跨战累积=2(run_ledger:42 判空早退缺陷消解的回归钉面——旧副本态恒 1)〔无状态化 V1〕", int(led.adventure_counters.get("adv_twin_digs", 0)) == 2)
|
||||
Kit.check("V1 第2战挖后 adventure 桶跨战累积=2(共享账判空早退缺陷消解的回归钉面——旧副本态恒 1)〔无状态化 V1〕", int(led.get("adv_twin_digs", 0)) == 2)
|
||||
# V6——battle 桶随宿主销毁:第1战标错落 insurance_used(battle 桶无前缀),次战新编排器读 0。
|
||||
var encA = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
|
||||
"feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()}
|
||||
"node_type": "battle", "adv_bucket": {}}
|
||||
var oA = RigS.new(_db, _fresh_rng(779401))
|
||||
oA.begin_battle(encA)
|
||||
var v6_rows: Array = []
|
||||
@@ -1031,7 +1043,7 @@ func _smoke_extended_rows() -> void:
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
|
||||
"feats": ["geologist"], "tree_rules": [], "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()}
|
||||
"node_type": "battle", "adv_bucket": {}}
|
||||
# ② 兑付行:有未揭示安全格 → 掷格揭示成功 → 扩展行带真实格与 fx_reveal 身份
|
||||
var o_fx = RigS.new(_db, _fresh_rng(779501))
|
||||
o_fx.begin_battle(enc_fx)
|
||||
|
||||
@@ -49,13 +49,13 @@ func _test_gate_predicate(db) -> void:
|
||||
Kit.check("前置:卓尔 max_hp=130(表值——难度乘数空表恒 1.0)", mhp == 130)
|
||||
# 未到阈值:hp = 阈值+1 → 不解锁,锁定条目不入可摇/保底集合
|
||||
ec.apply_damage(u, mhp - 66)
|
||||
Kit.check("门谓词:hp=66 > 阈值 65 → 零解锁", ec.check_skill_unlocks(u).is_empty())
|
||||
Kit.check("门谓词:hp=66 > 阈值 65 → 零解锁", ec.check_trigger_unlocks(u).is_empty())
|
||||
Kit.check("双面过滤:未解锁 venom_curb 不入可摇集合", not _has_sid(ec.rollable_skills(u), "venom_curb"))
|
||||
Kit.check("双面过滤:未解锁 venom_curb 不入保底特殊技集合", not _has_sid(ec.special_skills(u), "venom_curb"))
|
||||
# 恰到阈值:hp = 65 ≤ round(130×0.5) → 解锁(判式取 ≤)
|
||||
var hit65: bool = ec.apply_damage(u, 1)
|
||||
Kit.check("前置:补一刀至阈值 65 受理", hit65 and int(u.hp) == 65)
|
||||
var unlocked: Array = ec.check_skill_unlocks(u)
|
||||
var unlocked: Array = ec.check_trigger_unlocks(u)
|
||||
Kit.check("门谓词:hp=65 ≤ 阈值 65 → 恰解锁 venom_curb 一次", unlocked.size() == 1 and String(unlocked[0]) == "venom_curb")
|
||||
# 解锁即可摇(冷却初值 0)+ 保底特殊技集合同步可见
|
||||
Kit.check("解锁即可摇:venom_curb 入可摇集合(冷却初值 0)", _has_sid(ec.rollable_skills(u), "venom_curb"))
|
||||
@@ -74,28 +74,30 @@ func _test_latch_irreversible(db) -> void:
|
||||
var ec = _boss(db, 2, 9)
|
||||
var u: Dictionary = _boss_u(ec)
|
||||
ec.apply_damage(u, int(u.max_hp) / 2) # → 半血触发
|
||||
Kit.check("前置:半血触发解锁", ec.check_skill_unlocks(u).size() == 1)
|
||||
Kit.check("前置:半血触发解锁", ec.check_trigger_unlocks(u).size() == 1)
|
||||
ec.heal(u, int(u.max_hp)) # 抬回满血
|
||||
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_skill_unlocks(u).is_empty())
|
||||
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_trigger_unlocks(u).is_empty())
|
||||
Kit.check("闩不可逆:可摇集合仍含 venom_curb", _has_sid(ec.rollable_skills(u), "venom_curb"))
|
||||
|
||||
|
||||
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
|
||||
func _test_plain_rows_zero_drift(db) -> void:
|
||||
var ec = _boss(db, 1, 0, "battle")
|
||||
var u: Dictionary = _boss_u(ec)
|
||||
var ec = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
|
||||
var u: Dictionary = ec.all_enemies()[0]
|
||||
var any_unlock_key := false
|
||||
for e in u.pool:
|
||||
if e.has("unlock_hp_pct") or e.has("unlocked"):
|
||||
if e.has("trigger") or e.has("trigger_latched"):
|
||||
any_unlock_key = true
|
||||
Kit.check("零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)", not any_unlock_key)
|
||||
Kit.check("零扰动:满血复查零解锁", ec.check_skill_unlocks(u).is_empty())
|
||||
Kit.check("零扰动:满血复查零解锁", ec.check_trigger_unlocks(u).is_empty())
|
||||
var rollable: Array = ec.rollable_skills(u)
|
||||
var pool_count: int = (u.pool as Array).size()
|
||||
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
|
||||
# 同种子确定性:容器零随机——两实例同输入查询逐位同
|
||||
var ec2 = _boss(db, 1, 0, "battle")
|
||||
var u2: Dictionary = _boss_u(ec2)
|
||||
# 同种子确定性:容器零随机——两实例同输入查询逐位同(同款零扰动载体)
|
||||
var ec2 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
|
||||
var u2: Dictionary = ec2.all_enemies()[0]
|
||||
ec.apply_damage(u, 3)
|
||||
ec2.apply_damage(u2, 3)
|
||||
var a := JSON.stringify(ec.rollable_skills(u))
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 触发条件机制单测(技能池情境条件触发器批——V1 出域拒绝/V2 双面过滤/V3 保底不逼/V4 闩型语义/
|
||||
# V6 事实面供给/V7 池构成纪律/V8 低血防御/V9 停手偷袭/V10 缺省零扰动;V5 对拍由 skill_unlock_test
|
||||
# 迁移断言与 round_flow 编排集成断言承载)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/trigger_cond_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[trigger_cond_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)----
|
||||
var skill = db.skill
|
||||
var base_rows: Array = skill.rows.duplicate(true)
|
||||
var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5,
|
||||
"cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}}
|
||||
var bad_rows := [
|
||||
{"skill_id": "t_bad_fact", "display_name": "坏事实", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
|
||||
"trigger": {"fact": "nope", "op": "eq", "value": 1}},
|
||||
{"skill_id": "t_bad_op", "display_name": "坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
|
||||
"trigger": {"fact": "round_index", "op": "xor", "value": 1}},
|
||||
{"skill_id": "t_bad_key", "display_name": "坏键", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
|
||||
"trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": false, "extra": 1}},
|
||||
{"skill_id": "t_bad_latch", "display_name": "坏闩", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
|
||||
"trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": "yes"}},
|
||||
{"skill_id": "t_bad_enum_op", "display_name": "枚举坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
|
||||
"trigger": {"fact": "last_round_end", "op": "gt", "value": "stopped"}},
|
||||
{"skill_id": "t_bad_enum_val", "display_name": "枚举坏值", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
|
||||
"trigger": {"fact": "last_round_end", "op": "eq", "value": "win"}},
|
||||
{"skill_id": "t_bad_basic", "display_name": "普攻坏配", "type": "basic",
|
||||
"trigger": {"fact": "round_index", "op": "eq", "value": 1}},
|
||||
]
|
||||
for bad in bad_rows:
|
||||
skill.rows = base_rows.duplicate(true)
|
||||
skill.rows.append(bad)
|
||||
var errs: Array = skill.validate()
|
||||
var hit := false
|
||||
for e in errs:
|
||||
if String((e as Dictionary).get("category", "")) != "empty_table" and String((e as Dictionary).get("message", "")).contains(String(bad.get("skill_id", ""))):
|
||||
hit = true
|
||||
Kit.check_quiet(hit, "V1 出域拒绝:%s(%s)〔触发器批 V1〕" % [String(bad.get("skill_id", "")), String(bad.get("display_name", ""))])
|
||||
skill.rows = base_rows.duplicate(true)
|
||||
skill.rows.append(ok_row)
|
||||
Kit.check_quiet(skill.validate().is_empty(), "V1 合法触发行零 error〔触发器批 V1〕")
|
||||
skill.rows = base_rows
|
||||
|
||||
# ---- V2 门槛过滤双面(spawn 真实表→直调 rollable/special)----
|
||||
var ec = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check_quiet(ec.spawn(2, 9, 30, [], 8, 30, [], "", "", "boss"), "V2 前置:卓尔 Boss spawn")
|
||||
var u = ec.all_enemies()[0]
|
||||
Kit.check_quiet(int(u.hp) == int(u.max_hp), "V2 前置:满血(hp=max_hp)")
|
||||
var r_full: Array = ec.rollable_skills(u)
|
||||
var s_full: Array = ec.special_skills(u)
|
||||
Kit.check_quiet(not _has(r_full, "venom_curb") and not _has(s_full, "venom_curb"),
|
||||
"V2 条件假(满血):venom_curb 双面不入集合〔触发器批 V2〕")
|
||||
u["hp"] = 15
|
||||
ec.check_trigger_unlocks(u)
|
||||
Kit.check_quiet(_has(ec.rollable_skills(u), "venom_curb") and _has(ec.special_skills(u), "venom_curb"),
|
||||
"V2 条件真(半血过闩):venom_curb 双面入集合〔触发器批 V2〕")
|
||||
|
||||
# ---- V3 保底不逼条件不满足技(保底集合取 special_skills 已含排除——编排级:
|
||||
# 无条件特殊技 A w1+条件假技 B w9999+普攻;保底命中摇出的是 A,撤销过滤态同种子必中 B)----
|
||||
var ec_v3 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check_quiet(ec_v3.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V3 前置:地精 spawn")
|
||||
var g3 = ec_v3.all_enemies()[0]
|
||||
g3["pool"] = [
|
||||
{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
|
||||
{"skill_id": "heavy", "weight": 1, "cooldown_left": 0}, # A=无条件特殊技
|
||||
{"skill_id": "desperate_guard", "weight": 9999, "cooldown_left": 0,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}, # B=条件假技 w9999
|
||||
]
|
||||
g3["basic_streak"] = int(db.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
|
||||
var specials_v3: Array = ec_v3.special_skills(g3)
|
||||
Kit.check_quiet(_has(specials_v3, "heavy") and not _has(specials_v3, "desperate_guard"),
|
||||
"V3 保底集合含 A 排除 B(special_skills 真实入口——条件假技不逼出)〔触发器批 V3〕")
|
||||
var drawn: String = ""
|
||||
for i in range(30):
|
||||
var stream_v3 = _rng(700123).open_stream("behavior_roll")
|
||||
var pick = load("res://core/skill_lottery.gd").draw(specials_v3, stream_v3)
|
||||
if String(pick) == "desperate_guard":
|
||||
drawn = "desperate_guard"
|
||||
break
|
||||
Kit.check_quiet(drawn == "",
|
||||
"V3 保底摇号 30 次零逼出 B(SkillLottery 真实摇号——撤销过滤态 w9999 同种子必中,红/绿两态由撤销证明承载)〔触发器批 V3〕")
|
||||
|
||||
# ---- V4 闩型语义(过门恰一次/治疗抬血不回锁/阈值式判别 max_hp=8 value=35:hp=3 触发 hp=4 不触发)----
|
||||
Kit.check_quiet(int(u.hp) == 15 and ec.check_trigger_unlocks(u).is_empty(),
|
||||
"V4 闩已置:二次复查零新解锁(过门恰一次)〔触发器批 V4〕")
|
||||
u["hp"] = int(u.max_hp) # 治疗抬回满血
|
||||
Kit.check_quiet(ec.check_trigger_unlocks(u).is_empty() and _has(ec.rollable_skills(u), "venom_curb"),
|
||||
"V4 治疗抬血不回锁(单向闩)〔触发器批 V4〕")
|
||||
var ec8 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check_quiet(ec8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V4 前置:地精 spawn(hp 基线 8)")
|
||||
var g = ec8.all_enemies()[0]
|
||||
g["pool"] = [{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
|
||||
{"skill_id": "desperate_guard", "weight": 1, "cooldown_left": 0,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}]
|
||||
g["hp"] = 4
|
||||
Kit.check_quiet(_not_has(ec8.rollable_skills(g), "desperate_guard"),
|
||||
"V4 阈值式判别:hp=4 > T=round(8×35/100)=3 不入可摇集合〔触发器批 V4〕")
|
||||
g["hp"] = 3
|
||||
Kit.check_quiet(_has(ec8.rollable_skills(g), "desperate_guard"),
|
||||
"V4 阈值式判别:hp=3 ≤ T=3 入可摇集合(取整读数判别会在此漂移——阈值式钉死)〔触发器批 V4〕")
|
||||
|
||||
# ---- V6 事实面供给(负向 blast 递增/正向 blast 不递增/轮末转存/开战清零+facts 组包四键在位)----
|
||||
var RigS = load("res://core/battle_orchestrator.gd")
|
||||
var orch = RigS.new(db, _rng(700001))
|
||||
Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8,
|
||||
"hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [],
|
||||
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
|
||||
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0,
|
||||
"V6 开战清零:三计数全零起〔触发器批 V6〕")
|
||||
# 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件
|
||||
# ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits)
|
||||
var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1}
|
||||
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
|
||||
"display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0,
|
||||
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
|
||||
# 负向 blast(雷)递增
|
||||
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
|
||||
"display_name": "警觉", "reward_id": "enemy_alert"}, att_v6)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1,
|
||||
"V6 负向 blast(雷)递增恰一次〔触发器批 V6〕")
|
||||
|
||||
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
|
||||
var cv = load("res://core/config_db_cross_validator.gd").new(db)
|
||||
var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
|
||||
var comp_violations := 0
|
||||
for e in pool_errs:
|
||||
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
|
||||
comp_violations += 1
|
||||
Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕")
|
||||
var enemy_backup: Array = db.enemy.rows.duplicate(true)
|
||||
var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true)
|
||||
var cond_pool: Array[Variant] = []
|
||||
for entry in (cond_only_row.get("pool", []) as Array):
|
||||
var sid := String((entry as Dictionary).get("skill_id", ""))
|
||||
var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid)
|
||||
if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic":
|
||||
cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技)
|
||||
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
|
||||
cond_only_row["pool"] = cond_pool
|
||||
db.enemy.rows[0] = cond_only_row
|
||||
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
|
||||
var hit2 := false
|
||||
for e in errs2:
|
||||
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
|
||||
hit2 = true
|
||||
Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕")
|
||||
db.enemy.rows = enemy_backup
|
||||
|
||||
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
|
||||
var ec_v8 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check_quiet(ec_v8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V8 前置:地精 spawn")
|
||||
var g8 = ec_v8.all_enemies()[0]
|
||||
var has_dg := false
|
||||
for e in (g8.get("pool", []) as Array):
|
||||
if String((e as Dictionary).get("skill_id", "")) == "desperate_guard":
|
||||
has_dg = true
|
||||
Kit.check_quiet(has_dg and int(g8.max_hp) == 8,
|
||||
"V8 前置:地精池含孤注守势 w1(真实表读值——阈值 T=round(8×35/100)=3)〔触发器批 V8〕")
|
||||
Kit.check_quiet(_not_has(ec_v8.rollable_skills(g8), "desperate_guard"),
|
||||
"V8 hp=8>3 不入可摇集合〔触发器批 V8〕")
|
||||
g8["hp"] = 3
|
||||
Kit.check_quiet(_has(ec_v8.rollable_skills(g8), "desperate_guard"),
|
||||
"V8 hp=3≤3 入可摇集合(真实入口开轮摇号判据)〔触发器批 V8〕")
|
||||
|
||||
# ---- V9 示范投放:停手偷袭(钩爪恐魔池 w2+last_round_end eq stopped——facts 缺键=拦 fail-safe)----
|
||||
var ec_v9 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check_quiet(ec_v9.spawn(3, 1, 30, [], 8, 30, [], "", "", "battle"), "V9 前置:钩爪恐魔 spawn")
|
||||
var h9 = ec_v9.all_enemies()[0]
|
||||
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9), "sneak_ambush"),
|
||||
"V9 首轮(facts 缺 last_round_end 键=fail-safe 拦)不入集合〔触发器批 V9〕")
|
||||
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9, {"last_round_end": "cleared", "round_index": 2,
|
||||
"mines_hit_last": 0, "mines_hit_total": 0}), "sneak_ambush"),
|
||||
"V9 cleared 轮后不入集合〔触发器批 V9〕")
|
||||
Kit.check_quiet(_has(ec_v9.rollable_skills(h9, {"last_round_end": "stopped", "round_index": 2,
|
||||
"mines_hit_last": 0, "mines_hit_total": 0}),
|
||||
"sneak_ambush"), "V9 stopped 轮后入集合(真实表 w2)〔触发器批 V9〕")
|
||||
|
||||
# ---- V10 缺省零扰动(无 trigger 行的池快照逐键无新增+摇号同种子不变——快照键策略钉死)----
|
||||
var ec_v10 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check_quiet(ec_v10.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V10 前置:地精 spawn")
|
||||
var g10 = ec_v10.all_enemies()[0]
|
||||
var stray := 0
|
||||
for e in (g10.get("pool", []) as Array):
|
||||
for k in (e as Dictionary).keys():
|
||||
if not ["skill_id", "weight", "cooldown_left", "trigger", "trigger_latched"].has(String(k)):
|
||||
stray += 1
|
||||
Kit.check_quiet(stray == 0, "V10 无 trigger 行池快照零新键(快照键策略钉死——防未来给普行塞键)〔触发器批 V10〕")
|
||||
|
||||
quit(Kit.finish("trigger_cond_test", "PASS — 触发条件 V1/V2/V4/V6/V7/V8/V9/V10 全绿(V5 对拍由 skill_unlock/round_flow 编排集成承载)"))
|
||||
|
||||
|
||||
func _rng(seed_val: int):
|
||||
var r = load("res://core/rng_service.gd").new()
|
||||
r.set_master_seed(seed_val)
|
||||
return r
|
||||
|
||||
|
||||
func _has(arr: Array, sid: String) -> bool:
|
||||
for e in arr:
|
||||
if String((e as Dictionary).get("skill_id", "")) == sid:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _not_has(arr: Array, sid: String) -> bool:
|
||||
return not _has(arr, sid)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8xxbvy3vc40f
|
||||
@@ -55,9 +55,9 @@ func _smoke_adventure_api() -> void:
|
||||
# 入口停点观测(信号序 + 入口候选快照——容器写回规避 lambda 按值捕获)
|
||||
var sig_seq: Array = []
|
||||
var obs := {"entry_cands": []}
|
||||
adr.level_started.connect(_api_mark_started.bind(sig_seq))
|
||||
adr.choices_presented.connect(_api_mark_choices.bind(sig_seq, obs))
|
||||
adr.node_entered.connect(_api_mark_node.bind(sig_seq))
|
||||
# 直连面(转发信号退役):runner 信号在 level_started 回调内重连(构造期 runner 未建——
|
||||
# 信号序观测=先广播后连接的同步链承载)
|
||||
adr.level_started.connect(_api_mark_started.bind(sig_seq, obs, adr))
|
||||
Kit.check("冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))", adr.start_adventure(42))
|
||||
Kit.check("进行中重复 start_adventure 拒绝(重开须待结算完毕)", not adr.start_adventure(43))
|
||||
Kit.check("停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)", sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented")
|
||||
@@ -86,7 +86,12 @@ func _smoke_adventure_api() -> void:
|
||||
|
||||
|
||||
# 观测桩(信号序采集——bind 容器写回,签名按各信号实参定制)
|
||||
func _api_mark_started(_li: int, _dn: String, seq: Array) -> void:
|
||||
func _api_mark_started(_li: int, _dn: String, seq: Array, obs: Dictionary, adr) -> void:
|
||||
# 重取点重连(拆件批 T3 直连——回调时点 runner 已建;后续 choices_presented/node_entered 观测就位)
|
||||
if not adr.runner().choices_presented.is_connected(_api_mark_choices):
|
||||
adr.runner().choices_presented.connect(_api_mark_choices.bind(seq, obs))
|
||||
if not adr.runner().node_entered.is_connected(_api_mark_node):
|
||||
adr.runner().node_entered.connect(_api_mark_node.bind(seq))
|
||||
seq.append("level_started")
|
||||
|
||||
|
||||
@@ -178,7 +183,7 @@ func _smoke_adventure() -> void:
|
||||
rw_pc = (rw.get("cands", []) as Array)[0] as Dictionary
|
||||
var rw_pick_ok := false
|
||||
if not rw_pc.is_empty() and rw.get("adr") != null:
|
||||
rw_pick_ok = rw["adr"].apply_node_curio(String(rw_pc.get("kind", "")), String(rw_pc.get("tier", "")), String(rw_pc.get("feat_id", "")))
|
||||
rw_pick_ok = (rw["adr"] as AdventureRunner).stop().apply_node_curio(String(rw_pc.get("kind", "")), String(rw_pc.get("tier", "")), String(rw_pc.get("feat_id", "")))
|
||||
Kit.check("奖励三选一点选兑付受理(件=%s)" % String(rw_pc.get("kind", "")), rw_pick_ok)
|
||||
var enc_run: Dictionary = _drive_to_pref(autoplay, "battle")
|
||||
var enc_probe: Dictionary = enc_run.get("enc", {}) as Dictionary
|
||||
@@ -348,20 +353,20 @@ func _smoke_adventure() -> void:
|
||||
l2_price_bonus = int(pb.get("bonus", 0))
|
||||
Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus)
|
||||
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
|
||||
var acc_shop := int(a8.run_ledger.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子)
|
||||
var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子)
|
||||
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
|
||||
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
|
||||
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关)
|
||||
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)", not a8._curio_pending.is_empty())
|
||||
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not a8.stop()._pending.is_empty())
|
||||
Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level())
|
||||
var l2_cands: Array = a8._curio_pending["candidates"]
|
||||
var l2_cands: Array = a8.stop()._pending["candidates"]
|
||||
var l2_pick: Dictionary = l2_cands[0]
|
||||
Kit.check("三选一点选兑付受理(候选取首件)", a8.pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))))
|
||||
var picked2: String = String(a8._curio_result.get("picked", ""))
|
||||
var expect_acc: int = acc_shop + (int(a8._curio_result.get("value", 0)) if picked2 == "max_hp" else 0)
|
||||
Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.run_ledger.bonus.get("max_hp", -1)), acc_shop, picked2], int(a8.run_ledger.bonus.get("max_hp", -1)) == expect_acc)
|
||||
Kit.check("三选一点选兑付受理(候选取首件)", a8.stop().pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))))
|
||||
var picked2: String = String(a8.stop()._resolved.get("picked", ""))
|
||||
var expect_acc: int = acc_shop + (int(a8.stop()._resolved.get("value", 0)) if picked2 == "max_hp" else 0)
|
||||
Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [a8.journal().bonus_total("max_hp"), acc_shop, picked2], a8.journal().bonus_total("max_hp") == expect_acc)
|
||||
var max_eff8: int = a8.effective_max_hp() # 账本生效上限含调试浮窗 pct(a8 已 set 400)
|
||||
Kit.check("关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8], int(a8.ledger["hero_hp"]) <= max_eff8)
|
||||
Kit.check("关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [a8.stats().hero_hp, max_eff8], a8.stats().hero_hp <= max_eff8)
|
||||
Kit.check("兑付后推进受理(下一关开始)", a8.continue_to_next_level())
|
||||
# ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致)
|
||||
var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99))
|
||||
@@ -387,11 +392,11 @@ func _smoke_adventure() -> void:
|
||||
# 无种类次数上限,「达上限剔除」语义由牌库耗尽承载)
|
||||
var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11))
|
||||
Kit.check("剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)", a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}}))
|
||||
for di in range(a11._curio_deck.size() - 1, -1, -1):
|
||||
var dk := String((a11._curio_deck[di] as Dictionary).get("kind", ""))
|
||||
for di in range(a11._curio.deck.size() - 1, -1, -1):
|
||||
var dk := String((a11._curio.deck[di] as Dictionary).get("kind", ""))
|
||||
if dk == "attack" or dk == "ap":
|
||||
a11._curio_deck.remove_at(di)
|
||||
var draw: Array = a11._draw_curio_candidates()
|
||||
a11._curio.deck.remove_at(di)
|
||||
var draw: Array = a11.deck().draw_candidates()
|
||||
var draw_kinds := {}
|
||||
for t in draw:
|
||||
draw_kinds[String((t as Dictionary).get("kind", ""))] = true
|
||||
@@ -401,7 +406,7 @@ func _smoke_adventure() -> void:
|
||||
# white-box 测试属停点渠道,零投放零行为)
|
||||
var treasure_seen := false
|
||||
for i in 40:
|
||||
for t in a11._draw_curio_candidates():
|
||||
for t in a11.deck().draw_candidates():
|
||||
if String((t as Dictionary).get("kind", "")) == "treasure_item":
|
||||
treasure_seen = true
|
||||
break
|
||||
@@ -410,18 +415,18 @@ func _smoke_adventure() -> void:
|
||||
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
|
||||
a12.start_adventure(12)
|
||||
var cap := {"payload": {}}
|
||||
a12.curio_choice_presented.connect(func(p): cap["payload"] = p)
|
||||
var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0))
|
||||
a12.ledger["hero_hp"] = max_full
|
||||
a12.stop().curio_choice_presented.connect(func(p): cap["payload"] = p) # 停点件信号面(拆件批 T3 直连)
|
||||
var max_full: int = a12.stats().effective_max_hp() # 白板局无 override 无加算——置满口径=生效上限
|
||||
a12.stats().hero_hp = max_full
|
||||
a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
Kit.check("满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)", int(cap["payload"].get("heal_gained", -1)) == 0)
|
||||
# ⑬ 奖励节点共享池(同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现)
|
||||
var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13))
|
||||
Kit.check("节点共享池场景:冒险启动(seed 13)", a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}}))
|
||||
for di13 in range(a13._curio_deck.size() - 1, -1, -1):
|
||||
if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack":
|
||||
a13._curio_deck.remove_at(di13)
|
||||
var ndraw: Dictionary = a13.node_curio_candidates()
|
||||
for di13 in range(a13._curio.deck.size() - 1, -1, -1):
|
||||
if String((a13._curio.deck[di13] as Dictionary).get("kind", "")) == "attack":
|
||||
a13._curio.deck.remove_at(di13)
|
||||
var ndraw: Dictionary = a13.stop().node_curio_candidates()
|
||||
var ncands13: Array = ndraw.get("candidates", []) as Array
|
||||
var has_attack13 := false
|
||||
for t in ncands13:
|
||||
@@ -429,11 +434,11 @@ func _smoke_adventure() -> void:
|
||||
has_attack13 = true
|
||||
break
|
||||
Kit.check("奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" % str(ncands13), not has_attack13)
|
||||
a13.present_node_curio(ncands13)
|
||||
a13.present_node_curio([{"kind": "gp", "tier": "common"}])
|
||||
var gp13 := int(a13.ledger.get("gp", 0))
|
||||
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.apply_node_curio("gp", "common") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.curio_value("gp", "common")))
|
||||
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13._node_curio_pending.is_empty())
|
||||
a13.stop().present_node_curio(ncands13)
|
||||
a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}])
|
||||
var gp13: int = a13.wallet().gp
|
||||
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.adventure.curio_value("gp", "common")))
|
||||
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty())
|
||||
var s10: Dictionary = a10.summary
|
||||
Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)))
|
||||
|
||||
@@ -452,18 +457,18 @@ func _smoke_feats() -> void:
|
||||
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
|
||||
var feat_tokens := 0
|
||||
var tier_counts := {}
|
||||
for t in a_r0._curio_deck:
|
||||
for t in a_r0._curio.deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
feat_tokens += 1
|
||||
var tk := String((t as Dictionary).get("tier", ""))
|
||||
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
|
||||
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio_deck.size() == 132)
|
||||
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132)
|
||||
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
|
||||
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty())
|
||||
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("seal_burst").is_empty())
|
||||
# ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半)
|
||||
var kinds_ok := true
|
||||
for t in a_r0._curio_deck:
|
||||
for t in a_r0._curio.deck:
|
||||
var kk := String((t as Dictionary).get("kind", ""))
|
||||
if kk != "relic" and not _db.adventure.CURIO_KINDS.has(kk):
|
||||
kinds_ok = false
|
||||
@@ -505,18 +510,18 @@ func _smoke_feats() -> void:
|
||||
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
|
||||
Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72))
|
||||
var deck_relic := 0
|
||||
for t in a_r._curio_deck:
|
||||
for t in a_r._curio.deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
deck_relic += 1
|
||||
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio_deck.size() == 107)
|
||||
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio.deck.size() == 107)
|
||||
# 渠道白名单断言(渠道轴定位:关卡完成渠道只出专长/节点渠道零专长)
|
||||
var feat_seen: Dictionary = {}
|
||||
var node_relic_seen := false
|
||||
for i in 60:
|
||||
for t in a_r._draw_curio_candidates("level_clear"):
|
||||
for t in a_r.deck().draw_candidates("level_clear"):
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
feat_seen[String(t.get("feat_id", ""))] = true
|
||||
for t in a_r._draw_curio_candidates("reward_node"):
|
||||
for t in a_r.deck().draw_candidates("reward_node"):
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
node_relic_seen = true
|
||||
Kit.check("关卡完成渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)", feat_seen.size() >= 2)
|
||||
@@ -526,39 +531,39 @@ func _smoke_feats() -> void:
|
||||
a_t.start_adventure(91)
|
||||
var stock_before := {}
|
||||
for kk in ["attack", "gp", "heal"]:
|
||||
stock_before[kk] = a_t.pool_stock(kk)
|
||||
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]}
|
||||
Kit.check("A5 专长凭证兑付受理(节点渠道——拿到即开)", a_t.apply_node_curio("feat_ticket", "excellent"))
|
||||
var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array)
|
||||
stock_before[kk] = a_t.deck().pool_stock(kk)
|
||||
a_t.stop()._node_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]}
|
||||
Kit.check("A5 专长凭证兑付受理(节点渠道——拿到即开)", a_t.stop().apply_node_curio("feat_ticket", "excellent"))
|
||||
var tkt_cands: Array = (a_t.stop().node_curio_pending_state().get("candidates", []) as Array)
|
||||
var tkt_all_relic := tkt_cands.size() > 0
|
||||
for t in tkt_cands:
|
||||
if String((t as Dictionary).get("kind", "")) != "relic":
|
||||
tkt_all_relic = false
|
||||
Kit.check("A5 拿到即开——停点候选已替换为专长三选一(全 relic)", tkt_all_relic)
|
||||
Kit.check("A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)", a_t.pool_stock("attack") == int(stock_before["attack"]) and a_t.pool_stock("gp") == int(stock_before["gp"]))
|
||||
Kit.check("A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)", a_t.deck().pool_stock("attack") == int(stock_before["attack"]) and a_t.deck().pool_stock("gp") == int(stock_before["gp"]))
|
||||
Kit.check("A5 无专长凭证残留持有集", not (a_t.feats_held() as Array).has("feat_ticket"))
|
||||
var feats_n := (a_t.feats_held() as Array).size()
|
||||
a_t.apply_node_curio("relic", String((tkt_cands[0] as Dictionary).get("tier", "rare")), String((tkt_cands[0] as Dictionary).get("feat_id", "")))
|
||||
a_t.stop().apply_node_curio("relic", String((tkt_cands[0] as Dictionary).get("tier", "rare")), String((tkt_cands[0] as Dictionary).get("feat_id", "")))
|
||||
Kit.check("A5 专长面兑付受理(专长凭证开出候选可正常入持有集)", (a_t.feats_held() as Array).size() == feats_n + 1)
|
||||
# A6 保底:pity 直填 5 → 下次展示必出(第 6 次必出口径);限购满 → 恒不触发
|
||||
a_t._feat_ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
|
||||
a_t._feat_ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
a_t.stop()._deck.ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
|
||||
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
|
||||
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t._feat_ticket_consider(rg_t))
|
||||
Kit.check("A6 触发后清零", a_t._feat_ticket_pity == 0)
|
||||
a_t._feat_ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
|
||||
a_t._feat_ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
Kit.check("A6 限购满不再触发(保底冻结)", not a_t._feat_ticket_consider(rg_t))
|
||||
a_t._feat_ticket_sold = 0
|
||||
a_t._feat_ticket_pity = 0
|
||||
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().feat_ticket_consider(rg_t))
|
||||
Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0)
|
||||
a_t.stop()._deck.ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
|
||||
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t))
|
||||
a_t.stop()._deck.ticket_sold = 0
|
||||
a_t.stop()._deck.ticket_pity = 0
|
||||
var offered := false
|
||||
for i in 200:
|
||||
if a_t._feat_ticket_consider(rg_t):
|
||||
if a_t.deck().feat_ticket_consider(rg_t):
|
||||
offered = true
|
||||
break
|
||||
Kit.check("A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)", offered)
|
||||
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.pool_stock("relic") == deck_relic - 1)
|
||||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1)
|
||||
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主——
|
||||
# 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。
|
||||
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
|
||||
@@ -774,10 +779,10 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
return false
|
||||
if a._pending_next_level:
|
||||
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
|
||||
if not a._curio_pending.is_empty():
|
||||
var bcands: Array = a._curio_pending["candidates"]
|
||||
if not a.stop()._pending.is_empty():
|
||||
var bcands: Array = a.stop()._pending["candidates"]
|
||||
var bpick: Dictionary = autoplay._pick_token(bcands)
|
||||
a.pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
|
||||
a.stop().pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
|
||||
a.continue_to_next_level()
|
||||
continue
|
||||
var r = a.runner()
|
||||
@@ -791,12 +796,12 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
var t := String(r.current_node().get("type", ""))
|
||||
if t == "reward":
|
||||
# 奖励节点三选一(确定性选件、替换放弃新件、兑付确认回图;TP0 令牌化)
|
||||
var ndraw: Dictionary = a.node_curio_candidates()
|
||||
var ndraw: Dictionary = a.stop().node_curio_candidates()
|
||||
var ncands: Array = ndraw.get("candidates", []) as Array
|
||||
if not ncands.is_empty():
|
||||
a.present_node_curio(ncands)
|
||||
a.stop().present_node_curio(ncands)
|
||||
var npick: Dictionary = autoplay._pick_token(ncands)
|
||||
a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", "")))
|
||||
a.stop().apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", "")))
|
||||
r.continue_after_node()
|
||||
elif t == "rest":
|
||||
r.continue_after_node()
|
||||
@@ -836,10 +841,10 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary:
|
||||
if prefer == "battle":
|
||||
out["enc"] = r.current_battle().encounter
|
||||
else:
|
||||
var cands: Array = (adr.node_curio_candidates() as Dictionary).get("candidates", []) as Array
|
||||
adr.present_node_curio(cands)
|
||||
var cands: Array = (adr.stop().node_curio_candidates() as Dictionary).get("candidates", []) as Array
|
||||
adr.stop().present_node_curio(cands)
|
||||
out["cands"] = cands
|
||||
out["pending"] = not (adr._node_curio_pending as Dictionary).is_empty()
|
||||
out["pending"] = not (adr.stop()._node_pending as Dictionary).is_empty()
|
||||
return out
|
||||
var ids: Array = r.next_candidate_ids()
|
||||
if ids.is_empty():
|
||||
|
||||
@@ -93,9 +93,9 @@ func _init() -> void:
|
||||
adr_e.set_save_manager(sm_e)
|
||||
var params_e := {"hero_override": {}, "treasure_unlocked": {"wide_scout_band": true}}
|
||||
Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
adr_e._curio_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
|
||||
adr_e._curio.taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
|
||||
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var cands_e: Array = (adr_e._curio_pending.get("candidates", []) as Array).duplicate()
|
||||
var cands_e: Array = (adr_e.stop()._pending.get("candidates", []) as Array).duplicate()
|
||||
Kit.check_quiet(not cands_e.is_empty(), "P5 非末关的关卡完成三选一停点在案(直调 victory——L1 非末关)")
|
||||
var sm_f = SaveManagerScript.new(_bp_path)
|
||||
Kit.check_quiet(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
@@ -106,24 +106,24 @@ func _init() -> void:
|
||||
var got_cands := {"cands": [] as Array}
|
||||
var adr_f = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_f.set_save_manager(sm_f) # 兑付后停点快照落盘承载(与真机组合根同构)
|
||||
adr_f.curio_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate())
|
||||
adr_f.stop().curio_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate()) # 停点件信号面(拆件批 T3 直连)
|
||||
Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
|
||||
Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
|
||||
Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
|
||||
Kit.check_quiet(int(adr_f._curio_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
Kit.check_quiet(int(adr_f._curio.taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
var pick_e: Dictionary = cands_e[0]
|
||||
Kit.check_quiet(adr_f.pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理")
|
||||
var picked_e: String = String(adr_f._curio_result.get("picked", ""))
|
||||
if adr_f._curio_taken.has(picked_e):
|
||||
Kit.check_quiet(int(adr_f._curio_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
Kit.check_quiet(adr_f.stop().pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理")
|
||||
var picked_e: String = String(adr_f.stop()._resolved.get("picked", ""))
|
||||
if adr_f._curio.taken.has(picked_e):
|
||||
Kit.check_quiet(int(adr_f._curio.taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
|
||||
Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
|
||||
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var adr_g = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_g.set_save_manager(sm_f) # 已兑付态续玩的后续快照同承载
|
||||
var got_result := {"r": {}}
|
||||
adr_g.curio_resolved.connect(func(r): got_result["r"] = r.duplicate())
|
||||
adr_g.stop().curio_resolved.connect(func(r): got_result["r"] = r.duplicate()) # 停点件信号面(拆件批 T3 直连)
|
||||
Kit.check_quiet(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
|
||||
Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
|
||||
Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
|
||||
@@ -191,12 +191,12 @@ func _init() -> void:
|
||||
"P8 登记单源键集 == 写侧实拼断点键集(%d 键逐一对齐)〔wr2〕" % reg_keys.size())
|
||||
var frozen_keys := ["adventure_counters", "attunement_charges", "attune_refresh_y", "aux_items",
|
||||
"bonus", "bonus_deck", "bonus_pending", "bonus_result", "bonus_taken", "feat_ticket_pity",
|
||||
"feat_ticket_sold", "gp", "graph_gen_v2", "hero_hp", "keystone", "keystone_drops",
|
||||
"feat_ticket_sold", "gain_journal", "gp", "graph_gen_v2", "hero_hp", "keystone", "keystone_drops",
|
||||
"level_index", "master_seed", "node_id", "refresh_stock", "relics", "rng_counter",
|
||||
"shop_purchased", "shop_shelf", "shop_visit_buys", "visited_ids", "vision_counter_used"]
|
||||
frozen_keys.sort()
|
||||
Kit.check_quiet(reg_keys == frozen_keys,
|
||||
"P8 键集与冻结清单逐位一致(27 键契约;增删键即红)〔wr2〕")
|
||||
"P8 键集与冻结清单逐位一致(28 键契约——拆件批获得日记键为唯一声明扩展;增删键即红)〔wr2〕")
|
||||
var vocab := ["plain", "clamp", "keep_on_null", "dual_merge", "params_fallback", "token_migration"]
|
||||
var stray: Array = []
|
||||
for k in fields.keys():
|
||||
|
||||
@@ -20,30 +20,30 @@ func _init() -> void:
|
||||
# ---- ① 停点在途谓词:无停点时免费/付费刷新皆拒绝(付费路径扣费前置校验防空转烧钱包)----
|
||||
var adr = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr.start_adventure(1234, {"hero_override": {}, "treasure_unlocked": {}}), "冒险启动(种子 1234——刷新重抽需真实令牌池)")
|
||||
Kit.check_quiet(adr.curio_stop_pending() == false, "无停点=在途谓词 false")
|
||||
Kit.check_quiet(adr.refresh_curio() == false and adr.refresh_curio_paid() == false, "无停点:免费/付费刷新皆拒绝零副作用")
|
||||
Kit.check_quiet(adr.stop().curio_stop_pending() == false, "无停点=在途谓词 false")
|
||||
Kit.check_quiet(adr.stop().refresh_curio() == false and adr.stop().refresh_curio_paid() == false, "无停点:免费/付费刷新皆拒绝零副作用")
|
||||
|
||||
# ---- ② 免费位优先序 X→Y(白盒造关卡完成停点——smoke_all ⑫ 同构)----
|
||||
adr._curio_pending = {"candidates": _cands(), "payload": {}}
|
||||
Kit.check_quiet(adr.curio_stop_pending() == true, "关卡完成停点在途=谓词 true")
|
||||
adr._attune_refresh_x = 1
|
||||
adr._attune_refresh_y = 2
|
||||
Kit.check_quiet(adr.refresh_curio() and adr._attune_refresh_x == 0 and adr._attune_refresh_y == 2, "X 优先消耗(x1y2→x0y2)")
|
||||
Kit.check_quiet(adr.refresh_curio() and adr._attune_refresh_y == 1, "X 穷转 Y 池(y2→y1)")
|
||||
adr.stop()._pending = {"candidates": _cands(), "payload": {}}
|
||||
Kit.check_quiet(adr.stop().curio_stop_pending() == true, "关卡完成停点在途=谓词 true")
|
||||
adr.refresh_bank()._refresh_x = 1
|
||||
adr.refresh_bank()._refresh_y = 2
|
||||
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_x == 0 and adr.refresh_bank()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)")
|
||||
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_y == 1, "X 穷转 Y 池(y2→y1)")
|
||||
|
||||
# ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ----
|
||||
Kit.check_quiet(adr.curio_refresh_gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)")
|
||||
Kit.check_quiet(adr.refresh_curio_paid() and adr._attune_refresh_y == 1, "付费刷新不触碰免费位(y1 保持)")
|
||||
Kit.check_quiet(adr.curio_refresh_gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)")
|
||||
Kit.check_quiet(adr.refresh_curio_paid() and adr.curio_refresh_gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)")
|
||||
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)")
|
||||
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)")
|
||||
Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)")
|
||||
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)")
|
||||
|
||||
# ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)----
|
||||
adr.present_node_curio(_cands())
|
||||
Kit.check_quiet(adr._curio_paid_refreshes == 0 and adr.curio_refresh_gp_cost() == base, "节点停点重建=付费计数清零回基价")
|
||||
adr.refresh_curio_paid()
|
||||
Kit.check_quiet(adr._curio_paid_refreshes == 1, "付费刷新计数在途递增(=1)")
|
||||
adr.present_node_curio(_cands())
|
||||
Kit.check_quiet(adr.curio_refresh_gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)")
|
||||
adr.stop().present_node_curio(_cands())
|
||||
Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 0 and adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价")
|
||||
adr.stop().refresh_curio_paid()
|
||||
Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 1, "付费刷新计数在途递增(=1)")
|
||||
adr.stop().present_node_curio(_cands())
|
||||
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)")
|
||||
|
||||
# ---- ⑤ 钱包单口支出 spend_gp(LevelRunner——三选一付费档与商店刷新共用同一冒险钱包)----
|
||||
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
@@ -56,10 +56,10 @@ func _init() -> void:
|
||||
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
|
||||
var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)")
|
||||
Kit.check_quiet(adr_r._attune_refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
|
||||
Kit.check_quiet(adr_r.refresh_bank()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
|
||||
var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)")
|
||||
Kit.check_quiet(adr_o._attune_refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
|
||||
Kit.check_quiet(adr_o.refresh_bank()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
|
||||
|
||||
quit(Kit.finish("curio_refresh_paid"))
|
||||
|
||||
|
||||
@@ -26,54 +26,55 @@ func _init() -> void:
|
||||
Kit.check_quiet(false, "V9 前置:冒险启动")
|
||||
quit(Kit.finish("feat_vouchers"))
|
||||
return
|
||||
# V2——adventure 桶新冒险置空:start_adventure 后宿主字典为空(寿命随 RunLedger,无残留)
|
||||
Kit.check_quiet(a.run_ledger.adventure_counters.is_empty(),
|
||||
"V2:新冒险 adventure 桶置空(start_adventure 后 run_ledger.adventure_counters 为空)〔无状态化 V2〕")
|
||||
# V2——adventure 桶新冒险置空:start_adventure 后宿主字典为空(寿命随冒险应用层,无残留)
|
||||
Kit.check_quiet(a.adventure_counters.is_empty(),
|
||||
"V2:新冒险 adventure 桶置空(start_adventure 后宿主桶 adventure_counters 为空)〔无状态化 V2〕")
|
||||
|
||||
# V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量)
|
||||
a._reset_refresh_x() # X 归零基线(模拟新停点)
|
||||
Kit.check_quiet(a._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a._attune_refresh_x == 0,
|
||||
a.refresh_bank().reset_x() # X 归零基线(模拟新停点)
|
||||
Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank()._refresh_x == 0,
|
||||
"F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕")
|
||||
a._reset_refresh_x() # 再碰停点——X 重置但存量不吞
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a.attune_refresh_left() == 1,
|
||||
a.refresh_bank().reset_x() # 再碰停点——X 重置但存量不吞
|
||||
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == 1,
|
||||
"F-1 修复:停点重置后存量账存活(attune_refresh_left 含第三源)〔专长批 V9〕")
|
||||
# 消耗序:X 优先 → 存量(无 X 无 Y 时吃存量;refresh_curio 前置=在途停点)
|
||||
a._refresh_stock = 1
|
||||
a._attune_refresh_y = 0
|
||||
a._node_curio_pending = {"candidates": [{"kind": "attack", "tier": "common"}]}
|
||||
Kit.check_quiet(a.refresh_curio() and a.refresh_stock() == 0,
|
||||
a.refresh_bank()._refresh_stock = 1
|
||||
a.refresh_bank()._refresh_y = 0
|
||||
a.stop()._node_pending = {"candidates": [{"kind": "attack", "tier": "common"}]}
|
||||
Kit.check_quiet(a.stop().refresh_curio() and a.refresh_bank().refresh_stock() == 0,
|
||||
"刷新消耗序:X/Y 空时吃存量账〔专长批 V9〕")
|
||||
a._node_curio_pending = {}
|
||||
a.stop()._node_pending = {}
|
||||
# 摘牌:选中即消尽(禁早退路径零摘牌)
|
||||
var deck_refresh_before: int = a.pool_stock("refresh")
|
||||
var deck_refresh_before: int = a.deck().pool_stock("refresh")
|
||||
Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before)
|
||||
|
||||
# V9-2 储备金袋:待注入账 + 战首注入(节点级)
|
||||
Kit.check_quiet(a._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
|
||||
|
||||
# V9-3 时沙:下一战首轮 AP 队列(回合级池增量)
|
||||
Kit.check_quiet(a._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
|
||||
|
||||
# V23 两件出池(R1——同调电池删除/完美保险迁专长)
|
||||
Kit.check_quiet(not db.adventure.CURIO_KINDS.has("attune_battery")
|
||||
and not db.adventure.CURIO_KINDS.has("perfect_insurance"),
|
||||
"出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕")
|
||||
Kit.check_quiet(not a._apply_curio_pick("attune_battery", "rare", 1)
|
||||
and not a._apply_curio_pick("perfect_insurance", "rare", 1),
|
||||
Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1)
|
||||
and not a.stop()._apply_curio_pick("perfect_insurance", "rare", 1),
|
||||
"出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕")
|
||||
Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"),
|
||||
"电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕")
|
||||
Kit.check_quiet(int(db.adventure.curio_cap("attune_battery")) == 99,
|
||||
"出池后未知 kind 走分发表 default(cap 读数兜底 99——零崩溃面)〔卡② R1〕")
|
||||
# 账本保留(契约 1):主动件充能账与每冒险重置入口零改动
|
||||
# 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配)
|
||||
a._attunement = ["incense_stick"]
|
||||
a.run_ledger.attune_charges = {"incense_stick": 99}
|
||||
a._seed_attune_charges()
|
||||
Kit.check_quiet(int(a.run_ledger.attune_charges.get("incense_stick", -1)) != 99,
|
||||
"账本保留:删件后 _seed_attune_charges 仍按装备重置充能账(attune_charges 语义零变化)〔卡② R1〕")
|
||||
a._loadout.set_attunement(["incense_stick"])
|
||||
a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支)
|
||||
a._loadout_on_level_inventory() # 种行(首关 bind 时点语义)
|
||||
Kit.check_quiet(a._loadout.charges_left("incense_stick", a.runner().treasure_inventory) == 1,
|
||||
"账本保留:种行按装备表值投行(线香 battle 级 1——attune_charges 语义零变化)〔卡② R1〕")
|
||||
# V24 配额净删锚(R3——「净删」口径,rare 9→7/总量 104→102)
|
||||
var rare_sum := 0
|
||||
var rare_kinds: Array[String] = []
|
||||
@@ -88,34 +89,34 @@ func _init() -> void:
|
||||
"配额锚:稀有档合计 7、投放品种={ap,rap,time_sand}、令牌池总量 102(validate 硬断言同批)〔卡② R3〕")
|
||||
Kit.check_quiet(db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
|
||||
|
||||
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)
|
||||
a._curio_deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
|
||||
a._curio_pending = {"candidates": a._curio_deck.duplicate()}
|
||||
Kit.check_quiet(a.pick_curio("rap", "rare") and a.pool_stock("rap") == 0,
|
||||
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)〔拆件批 V2——cs3 凭证拿到即开现役断言承继〕
|
||||
a._curio.deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
|
||||
a.stop()._pending = {"candidates": a._curio.deck.duplicate()}
|
||||
Kit.check_quiet(a.stop().pick_curio("rap", "rare") and a.deck().pool_stock("rap") == 0,
|
||||
"凭证令牌:pick_curio 摘牌(选中踢池——与属性件同语义)〔专长批 V9〕")
|
||||
|
||||
# V10-1 留任 2 件路由与值不变:gp 钱包/heal 当即回血(每次点选独立停点态)
|
||||
a._curio_deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}]
|
||||
a._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
var gp_taken_before := int(a._curio_taken.get("gp", 0))
|
||||
Kit.check_quiet(a.pick_curio("gp", "common") and int(a._curio_taken.get("gp", 0)) == gp_taken_before + 1,
|
||||
a._curio.deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}]
|
||||
a.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
var gp_taken_before := int(a._curio.taken.get("gp", 0))
|
||||
Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a._curio.taken.get("gp", 0)) == gp_taken_before + 1,
|
||||
"留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕")
|
||||
var hp_before := int(a.ledger.get("hero_hp", 0))
|
||||
a._curio_pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
|
||||
Kit.check_quiet(a.pick_curio("heal", "excellent") and int(a.ledger.get("hero_hp", 0)) == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
|
||||
var hp_before: int = a.stats().hero_hp
|
||||
a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
|
||||
Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.stats().hero_hp == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
|
||||
"留任应急口粮:当即回血 +6 封顶生效上限〔专长批 V10〕")
|
||||
# V10-2 行动点上限归属性轴:ap kind 走加算区第四轴
|
||||
a._curio_deck = [{"kind": "ap", "tier": "rare"}]
|
||||
a._curio_pending = {"candidates": a._curio_deck.duplicate()}
|
||||
var ap_bonus_before := int(a.run_ledger.bonus.get("ap", 0))
|
||||
Kit.check_quiet(a.pick_curio("ap", "rare") and int(a.run_ledger.bonus.get("ap", 0)) == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
|
||||
a._curio.deck = [{"kind": "ap", "tier": "rare"}]
|
||||
a.stop()._pending = {"candidates": a._curio.deck.duplicate()}
|
||||
var ap_bonus_before: int = a.journal().bonus_total("ap")
|
||||
Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.journal().bonus_total("ap") == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
|
||||
"行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕")
|
||||
|
||||
# V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构)
|
||||
var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json")
|
||||
sm.load_from_disk()
|
||||
a.run_ledger.adventure_counters = {"pain_layers": 3, "adv_twin_digs": 5} # 四账住 RunLedger
|
||||
a._refresh_stock = 2
|
||||
a.adventure_counters = {"pain_layers": 3, "adv_twin_digs": 5} # 冒险桶宿主字典直写(测试构造面)
|
||||
a.refresh_bank()._refresh_stock = 2
|
||||
a.set_save_manager(sm)
|
||||
a._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var bp16: Dictionary = sm.breakpoint_snapshot()
|
||||
@@ -138,7 +139,7 @@ func _init() -> void:
|
||||
var b4 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(b4.resume_adventure(bp_legacy2, {}),
|
||||
"V13:旧档 adventure_counters 含改名前旧键续玩受理零崩溃(无迁移——旧键留桶成死键)〔卡② V13/H1〕")
|
||||
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2,
|
||||
Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_bank().refresh_stock() == 2,
|
||||
"V16:续玩回填逐项一致(adventure 桶随断点往返·计数恢复)〔无状态化 V3〕")
|
||||
# V25 完美保险专长验收组(R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
|
||||
var fi: Dictionary = db.feats.row_by_id("perfect_insurance")
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 主角账三件单测(领域拆分批——hr1 公式逐值/hr1-s 商店加算同值/hr2 三通道日记与断点片段;
|
||||
# HeroStats 数值域+GpWallet 钱包域+GainJournal 加算日记域逐件直构,另附源码断言面)。
|
||||
# 公式判据值全部由表读值推导(禁数值字面量断言);真链段走 start_adventure 与 runner 接口③。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/hero_ledger_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const HeroStatsScript := preload("res://core/hero_stats.gd")
|
||||
const GpWalletScript := preload("res://core/gp_wallet.gd")
|
||||
const GainJournalScript := preload("res://core/gain_journal.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const LevelRunnerScript := preload("res://core/level_runner.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[hero_ledger_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# ---- hr1:生效公式逐值(表值+永久强化 flat+加算区后乘调试 pct——与改前同式)----
|
||||
var journal_a = GainJournalScript.new()
|
||||
var hero = HeroStatsScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2}, journal_a)
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5,
|
||||
"hr1 生效上限 = 表值+永久强化 flat(pct=0 乘法恒等)")
|
||||
Kit.check_quiet(hero.effective_atk() == int(db.hero.atk) + 3,
|
||||
"hr1 生效攻击 = 表值+永久强化 flat")
|
||||
Kit.check_quiet(hero.effective_ac() == int(db.forge_armor_route_by_type(db.forge_default_armor_type()).get("base_ac", 0)) + 2,
|
||||
"hr1 生效护甲 = 甲型基础 AC+甲路线等级加值(纯 flat 无 pct)")
|
||||
hero.set_debug_override(50, 50)
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.hero.max_hp) + 5) * 1.5)),
|
||||
"hr1 调试 pct=50 后乘——上限 ×1.5(叠加次序:flat 先加 pct 后乘)")
|
||||
hero.set_debug_override(0, 0)
|
||||
journal_a.gain_bonus("max_hp", 4, "battle_export", "max_hp")
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5 + 4,
|
||||
"hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加——公式消费日记汇总)")
|
||||
|
||||
# hr1 回血封顶公式逐值(算术宿主=HeroStats.rest_heal 单点——改前 LevelRunner.rest_heal 同式)
|
||||
Kit.check_quiet(HeroStatsScript.rest_heal(30, 25, 26) == mini(int(floor(26.0 * 30.0 / 100.0)), 26 - 25),
|
||||
"hr1 回血 = floor(上限×比例÷100) 后封顶缺口(比例部分 7 > 缺口 1 → 1)")
|
||||
Kit.check_quiet(HeroStatsScript.rest_heal(30, 10, 26) == int(floor(26.0 * 30.0 / 100.0)),
|
||||
"hr1 回血比例部分未触封顶时按比例整值")
|
||||
|
||||
# ---- hr1 真链段:start_adventure 置满+接口③治疗封顶回报(runner._rest_handler → 数值账公式)----
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(1234)
|
||||
var adr = AdventureRunnerScript.new(db, rng)
|
||||
Kit.check_quiet(adr.start_adventure(1234, {}), "hr1 真链冒险启动(seed 1234)")
|
||||
var runner = adr.runner()
|
||||
Kit.check_quiet(runner.hero_hp == adr.stats().effective_max_hp(),
|
||||
"hr1 冒险开始生命账本置满生效上限(含 pct 口径——与战斗内同源)")
|
||||
var healed: int = adr.apply_rest(30, runner.hero_hp, runner.effective_max_hp()) # 接口③真链(关卡恢复点兑付的同一 handler)
|
||||
Kit.check_quiet(healed == HeroStatsScript.rest_heal(30, runner.hero_hp - healed, runner.effective_max_hp()),
|
||||
"hr1 接口③真链回报 = 数值账回血公式(恢复点兑付同链)")
|
||||
|
||||
# ---- hr1-s:商店属性商品加算走日记单写原语(购买后 bonus_total 与旧径同值)----
|
||||
runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例)
|
||||
var products: Array = runner.shop_products()
|
||||
var bought := false
|
||||
for p in products:
|
||||
if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "":
|
||||
var attr := String(p.get("attr", ""))
|
||||
var before: int = runner.journal().bonus_total(attr)
|
||||
bought = runner.buy_product(String(p.get("product_id", "")))
|
||||
if bought:
|
||||
Kit.check_quiet(runner.journal().bonus_total(attr) == before + int(p.get("flat_value", 0)),
|
||||
"hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)")
|
||||
Kit.check_quiet(runner.journal().purchase_tier(String(p.get("product_id", ""))) == 1,
|
||||
"hr1-s 购买档数记日记(递增定价输入单点)")
|
||||
break
|
||||
Kit.check_quiet(bought, "hr1-s 真链购买受理(GP 充足路径)")
|
||||
|
||||
# ---- hr2:三通道日记(明细来源指回+汇总同步维护)----
|
||||
var h2 = GainJournalScript.new()
|
||||
h2.gain_bonus("atk", 5, "curio_pick", "attack")
|
||||
h2.gain_bonus("atk", 2, "shop_product", "p_atk_1")
|
||||
h2.gain_bonus("armor_class", 1, "battle_export", "armor_class")
|
||||
Kit.check_quiet(h2.bonus_total("atk") == 7 and h2.bonus_total("armor_class") == 1,
|
||||
"hr2 三通道汇总累计(三选一件+商店+战末同落加算区)")
|
||||
var log_arr: Array = h2._gain_log
|
||||
Kit.check_quiet(log_arr.size() == 3, "hr2 获得记录列表三通道各一条")
|
||||
Kit.check_quiet(String((log_arr[0] as Dictionary).get("source_class", "")) == "curio_pick"
|
||||
and String((log_arr[1] as Dictionary).get("source_class", "")) == "shop_product"
|
||||
and String((log_arr[2] as Dictionary).get("source_class", "")) == "battle_export",
|
||||
"hr2 记录来源指回正确(curio_pick/shop_product/battle_export 词表)")
|
||||
var details: Array = h2._bonus_detail
|
||||
Kit.check_quiet(details.size() == 3 and int((details[0] as Dictionary).get("gain_id", 0)) == int((log_arr[0] as Dictionary).get("id", -1)),
|
||||
"hr2 明细带来源指回(gain_id ↔ 记录 id)")
|
||||
Kit.check_quiet(h2.keystone_gained() == 0 and h2.vision_used() == 0,
|
||||
"hr2 钥石/真视零通道时零账")
|
||||
|
||||
# hr2 钥石/真视/购买记账
|
||||
h2.record_keystone_drop({"level": 1, "first": true, "amount": 3})
|
||||
Kit.check_quiet(h2.keystone_gained() == 3 and (h2.keystone_drop_log()[0] as Dictionary).get("amount", 0) == 3,
|
||||
"hr2 钥石逐杀明细+累计单写")
|
||||
h2.record_vision_use()
|
||||
Kit.check_quiet(h2.vision_used() == 1, "hr2 真视使用恰计一次")
|
||||
|
||||
# hr2 断点片段往返(日记在档=现行事实源)——三件各交各键、并集拼装
|
||||
var frag: Dictionary = {}
|
||||
frag.merge(h2.breakpoint_fragment())
|
||||
var w2 = GpWalletScript.new()
|
||||
w2.gain_gp(37)
|
||||
frag.merge(w2.breakpoint_fragment())
|
||||
var s2 = HeroStatsScript.new(db, {}, GainJournalScript.new())
|
||||
s2.hero_hp = 21
|
||||
s2.feats.append("twin_pick")
|
||||
frag.merge(s2.breakpoint_fragment())
|
||||
var j3 = GainJournalScript.new()
|
||||
var w3 = GpWalletScript.new()
|
||||
var s3 = HeroStatsScript.new(db, {}, j3)
|
||||
j3.restore_fragment(frag, {})
|
||||
w3.restore_fragment(frag, {})
|
||||
s3.restore_fragment(frag, {})
|
||||
Kit.check_quiet(j3.bonus_total("atk") == 7 and j3.bonus_total("armor_class") == 1,
|
||||
"hr2 片段往返:日记在档由明细推导汇总(逐轴相等)")
|
||||
Kit.check_quiet(w3.gp == 37 and j3.keystone_gained() == 3 and j3.vision_used() == 1
|
||||
and s3.hero_hp == 21 and (s3.feats_held() as Array).has("twin_pick"),
|
||||
"hr2 片段往返:三件账面键逐值(生命/特性/钱包/钥石/真视)")
|
||||
Kit.check_quiet((j3._gain_log as Array).size() == 3 and (j3._bonus_detail as Array).size() == 3,
|
||||
"hr2 片段往返:日记全量还原")
|
||||
|
||||
# hr2 旧档回退(无日记键→汇总直载跳过比对)
|
||||
var legacy: Dictionary = frag.duplicate(true)
|
||||
legacy.erase("gain_journal")
|
||||
var j4 = GainJournalScript.new()
|
||||
j4.restore_fragment(legacy, {})
|
||||
Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 and (j4._gain_log as Array).is_empty(),
|
||||
"hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)")
|
||||
|
||||
# hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填)
|
||||
var tampered: Dictionary = frag.duplicate(true)
|
||||
(tampered["bonus"] as Dictionary)["atk"] = 99
|
||||
var j5 = GainJournalScript.new()
|
||||
j5.restore_fragment(tampered, {})
|
||||
Kit.check_quiet(j5.bonus_total("atk") == 0,
|
||||
"hr2 汇总与日记推导不符→拒载(汇总不回填——日记为现行事实源)")
|
||||
|
||||
# ---- sc3 源码断言(拆件批 T3——构造注入收口与转发族退役的静态面)----
|
||||
var adv_src := FileAccess.get_file_as_string("res://core/adventure_runner.gd")
|
||||
var lvl_src := FileAccess.get_file_as_string("res://core/level_runner.gd")
|
||||
var bind_shop_adv: int = adv_src.count("func _bind_shop")
|
||||
var bind_shop_lvl: int = lvl_src.count("func _bind_shop")
|
||||
var bind_calls := 0 # 调用行计数(排除 func 定义行——行首剥缩进后以 _bind_shop() 起始)
|
||||
for line in (adv_src + lvl_src).split("\n"):
|
||||
var t := line.strip_edges()
|
||||
if t.begins_with("_bind_shop()"):
|
||||
bind_calls += 1
|
||||
Kit.check_quiet(bind_shop_adv == 0 and bind_shop_lvl == 1,
|
||||
"sc3 _bind_shop 定义全仓仅 level_runner 构造一处(商店重绑机制退役)〔拆件批 V5〕")
|
||||
Kit.check_quiet(bind_calls == 1,
|
||||
"sc3 _bind_shop 调用全仓仅构造一处(bind_calls=%d)〔拆件批 V5〕" % bind_calls)
|
||||
Kit.check_quiet(not adv_src.contains("_forward_") and not adv_src.contains("signal node_entered"),
|
||||
"sc3 六转发信号族与 _forward_ 转发方法全仓零命中(信号直连收口)〔拆件批 V5〕")
|
||||
# al4 源码断言(配装批——RunLedger 整类退役全仓零引用)
|
||||
var runledger_hits := 0
|
||||
for f in ["res://core/adventure_runner.gd", "res://core/level_runner.gd", "res://core/battle_orchestrator.gd",
|
||||
"res://core/shop_session.gd", "res://core/effect_engine.gd", "res://core/effect_context.gd"]:
|
||||
runledger_hits += FileAccess.get_file_as_string(f).count("RunLedger") + FileAccess.get_file_as_string(f).count("run_ledger")
|
||||
Kit.check_quiet(runledger_hits == 0 and not FileAccess.file_exists("res://core/run_ledger.gd"),
|
||||
"al4 RunLedger 全仓零引用+类文件已删(充能单源化退役收口)〔配装批 V4〕")
|
||||
# hs1 源码断言(领域拆分批——hero_ledger 整类退役全仓零引用)
|
||||
var heroledger_hits := 0
|
||||
for f in ["res://core/adventure_runner.gd", "res://core/level_runner.gd", "res://core/battle_orchestrator.gd",
|
||||
"res://core/shop_session.gd", "res://core/curio_stop.gd"]:
|
||||
heroledger_hits += FileAccess.get_file_as_string(f).count("HeroLedger") + FileAccess.get_file_as_string(f).count("hero_ledger")
|
||||
Kit.check_quiet(heroledger_hits == 0 and not FileAccess.file_exists("res://core/hero_ledger.gd"),
|
||||
"hs1 hero_ledger 全仓零引用+类文件已删(主角账三件拆分退役收口)〔领域拆分批 V1〕")
|
||||
|
||||
quit(Kit.finish("hero_ledger_test", "PASS — 主角账三件公式/三通道日记/断点片段往返与旧档回退/源码断言 全绿"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1motti55hwib
|
||||
@@ -203,9 +203,9 @@ func _test_resume_rebuild(db) -> void:
|
||||
t.info_visible_layers = 2
|
||||
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}]
|
||||
db.level = t
|
||||
var ledger := {"hero_hp": 10, "gp": 0}
|
||||
var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false,
|
||||
resumed.hero_hp = 10 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
|
||||
var ok: bool = resumed.start_level_external(1, Callable(), false,
|
||||
{"node_id": 2, "visited_ids": [0, 1, 2]})
|
||||
Kit.check_quiet(ok, "续玩用例:resume_state 重建停点成功")
|
||||
if not ok:
|
||||
|
||||
@@ -721,8 +721,7 @@ func _smoke_level_graph() -> void:
|
||||
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
|
||||
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
|
||||
var vis_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(ACTIVE_GRAPH_SEED))
|
||||
var vis_ledger := {"hero_hp": 10, "gp": 0}
|
||||
var vis_ok: bool = vis_runner.start_level_external(2, vis_ledger, {}, Callable(), {}, true)
|
||||
var vis_ok: bool = vis_runner.start_level_external(2, Callable(), true)
|
||||
Kit.check("可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)", vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1)
|
||||
var vis_ids: Array = vis_runner.type_visible_ids()
|
||||
var vis_cand_in := true
|
||||
@@ -1033,8 +1032,8 @@ func _smoke_shop_room() -> void:
|
||||
var gp0: int = runner.gp
|
||||
Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up"))
|
||||
Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6)
|
||||
# 加算区住 RunLedger
|
||||
Kit.check("统一冒险加算区 atk +1(bonus={atk,max_hp,armor_class} 账本)", int(runner.run_ledger.bonus.get("atk", -1)) == 1 and int(runner.run_ledger.bonus.get("max_hp", -1)) == 0 and int(runner.run_ledger.bonus.get("armor_class", -1)) == 0)
|
||||
# 加算区住加算日记
|
||||
Kit.check("统一冒险加算区 atk +1(加算区住加算日记)", runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0)
|
||||
by_id = _shop_products_by_id(runner.shop_products())
|
||||
Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1)
|
||||
# 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道)
|
||||
@@ -1318,12 +1317,12 @@ func _smoke_shop_pool() -> void:
|
||||
var tier := String(buy_c.get("tier", ""))
|
||||
var val_map := {"attack": "atk", "max_hp": "max_hp", "armor": "armor_class", "ap": "ap"}
|
||||
var gp0: int = int(r8.gp)
|
||||
var taken0: int = int(a8._curio_taken.get(kind, 0))
|
||||
var val0: int = int(a8.run_ledger.bonus.get(String(val_map[kind]), -999))
|
||||
var taken0: int = int(a8._curio.taken.get(kind, 0))
|
||||
var val0: int = a8.journal().bonus_total(String(val_map[kind]))
|
||||
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
|
||||
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], int(a8.run_ledger.bonus.get(String(val_map[kind]), -999)) == val0 + int(_db.adventure.curio_value(kind, tier)))
|
||||
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio_taken.get(kind, 0))], int(a8._curio_taken.get(kind, 0)) == taken0 + 1)
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)))
|
||||
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1)
|
||||
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
|
||||
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
|
||||
r8.gp = int(r8.gp) + 64
|
||||
@@ -1348,9 +1347,9 @@ func _smoke_shop_pool() -> void:
|
||||
cut_seen = true
|
||||
Kit.check("切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)", not cut_seen)
|
||||
# ⑤ 池空硬闸(库存尽剔出牌库——TP0:white-box 耗尽 attack 令牌后货架零 attack + 消费口拒绝)
|
||||
for di8 in range(a8._curio_deck.size() - 1, -1, -1):
|
||||
if String((a8._curio_deck[di8] as Dictionary).get("kind", "")) == "attack":
|
||||
a8._curio_deck.remove_at(di8)
|
||||
for di8 in range(a8._curio.deck.size() - 1, -1, -1):
|
||||
if String((a8._curio.deck[di8] as Dictionary).get("kind", "")) == "attack":
|
||||
a8._curio.deck.remove_at(di8)
|
||||
var attack_seen := false
|
||||
for i in 40:
|
||||
r8._shop_draw_shelf()
|
||||
@@ -1358,13 +1357,13 @@ func _smoke_shop_pool() -> void:
|
||||
attack_seen = true
|
||||
break
|
||||
Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen)
|
||||
var stock_before: int = a8.pool_stock("attack")
|
||||
Kit.check("池空消费拒绝(handler 零副作用——买光即枯竭硬闸)", not a8._consume_pool_purchase("attack", "common") and a8.pool_stock("attack") == stock_before)
|
||||
var stock_before: int = a8.deck().pool_stock("attack")
|
||||
Kit.check("池空消费拒绝(handler 零副作用——买光即枯竭硬闸)", not a8.stop().consume_pool_purchase("attack", "common") and a8.deck().pool_stock("attack") == stock_before)
|
||||
# ⑥ 跨渠道共库 shop→reward 向:回填一枚 attack 令牌 → 消费口受理 → 奖励牌库同步摘牌
|
||||
a8._curio_deck.append({"kind": "attack", "tier": "common"})
|
||||
Kit.check("商店消费口受理(_apply_curio_pick 同口——真单点库)", a8._consume_pool_purchase("attack", "common"))
|
||||
a8._curio.deck.append({"kind": "attack", "tier": "common"})
|
||||
Kit.check("商店消费口受理(停点件兑付单点同口——真单点库)〔拆件批 V3〕", a8.stop().consume_pool_purchase("attack", "common"))
|
||||
var reward_attack_seen := false
|
||||
for t6 in a8._draw_curio_candidates():
|
||||
for t6 in a8.deck().draw_candidates():
|
||||
if String((t6 as Dictionary).get("kind", "")) == "attack":
|
||||
reward_attack_seen = true
|
||||
break
|
||||
@@ -1405,10 +1404,10 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
return false
|
||||
if a._pending_next_level:
|
||||
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
|
||||
if not a._curio_pending.is_empty():
|
||||
var bcands: Array = a._curio_pending["candidates"]
|
||||
if not a.stop()._pending.is_empty():
|
||||
var bcands: Array = a.stop()._pending["candidates"]
|
||||
var bpick: Dictionary = autoplay._pick_token(bcands)
|
||||
a.pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
|
||||
a.stop().pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
|
||||
a.continue_to_next_level()
|
||||
continue
|
||||
var r = a.runner()
|
||||
@@ -1422,12 +1421,12 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
var t := String(r.current_node().get("type", ""))
|
||||
if t == "reward":
|
||||
# 奖励节点三选一(确定性选件、替换放弃新件、兑付确认回图;TP0 令牌化)
|
||||
var ndraw: Dictionary = a.node_curio_candidates()
|
||||
var ndraw: Dictionary = a.stop().node_curio_candidates()
|
||||
var ncands: Array = ndraw.get("candidates", []) as Array
|
||||
if not ncands.is_empty():
|
||||
a.present_node_curio(ncands)
|
||||
a.stop().present_node_curio(ncands)
|
||||
var npick: Dictionary = autoplay._pick_token(ncands)
|
||||
a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", "")))
|
||||
a.stop().apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", "")))
|
||||
r.continue_after_node()
|
||||
elif t == "rest":
|
||||
r.continue_after_node()
|
||||
|
||||
@@ -287,20 +287,20 @@ func _meta_settle_and_workshop() -> void:
|
||||
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
|
||||
Kit.check("充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕",
|
||||
a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}))
|
||||
Kit.check("充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)", # 充能账住 RunLedger
|
||||
int(a12.run_ledger.attune_charges.get("incense_stick", -1)) == 1 and int(a12.run_ledger.attune_charges.get("sky_eye", -1)) == 1)
|
||||
Kit.check("充能按装备 active_charged 件种行(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面;读数=行派生)",
|
||||
a12._loadout.charges_left("incense_stick", a12.runner().treasure_inventory) == 1 and a12._loadout.charges_left("sky_eye", a12.runner().treasure_inventory) == 1)
|
||||
a12._attunement = []
|
||||
a12._attune_refresh_x = 1
|
||||
a12._attune_refresh_y = 2
|
||||
a12._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
|
||||
a12.refresh_bank()._refresh_x = 1
|
||||
a12.refresh_bank()._refresh_y = 2
|
||||
a12.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)",
|
||||
a12.refresh_curio() and a12._attune_refresh_x == 0 and a12._attune_refresh_y == 2)
|
||||
a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2)
|
||||
Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)",
|
||||
a12.refresh_curio() and a12.refresh_curio() and a12._attune_refresh_y == 0
|
||||
and a12.refresh_curio() == false)
|
||||
a12._reset_refresh_x()
|
||||
a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0
|
||||
and a12.stop().refresh_curio() == false)
|
||||
a12.refresh_bank().reset_x()
|
||||
Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕",
|
||||
a12._attune_refresh_x == 0 and a12.attune_refresh_left() == 0)
|
||||
a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0)
|
||||
# ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾
|
||||
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
|
||||
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
|
||||
@@ -365,7 +365,7 @@ func _meta_injection() -> void:
|
||||
int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)))
|
||||
Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档",
|
||||
int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
|
||||
and int(adr.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
|
||||
and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
|
||||
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
|
||||
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251))
|
||||
@@ -511,10 +511,10 @@ func _smoke_forge_axes() -> void:
|
||||
Kit.check("入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕",
|
||||
int(r5.effective_ac()) == 12 + 1)
|
||||
Kit.check("参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕",
|
||||
adr5.hero_attack_type == "normal" and adr5.hero_armor_type == "heavy")
|
||||
adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy")
|
||||
# V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切)
|
||||
Kit.check("冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕",
|
||||
mp5.forge_select_armor("light") and adr5.hero_armor_type == "heavy"
|
||||
mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy"
|
||||
and int(r5.effective_ac()) == 12 + 1)
|
||||
# V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键)
|
||||
var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"})
|
||||
@@ -728,10 +728,11 @@ func _smoke_tree_rules() -> void:
|
||||
int(orch2.action_economy.pending_ap_delta()) == 0)
|
||||
# 树限次按关清零改由 LevelRunner.level_counters 随 new 承载(旧硬编码名单冒烟断言随函数删除——换代同生死;新语义由 V4/V5 段承载)
|
||||
# 财富之道·散:商品标价折价(level_runner 计价单点)
|
||||
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004))
|
||||
lr.set_tree_rules([])
|
||||
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004),
|
||||
{"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr._spend_discounted(100)) == 100)
|
||||
lr.set_tree_rules(["economy_spend"])
|
||||
lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005),
|
||||
{"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
|
||||
int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1)
|
||||
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
|
||||
|
||||
@@ -35,11 +35,11 @@ func _init() -> void:
|
||||
# T10——空配置验收闸(三选一池空兜底/商店空货架)
|
||||
var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
e.start_adventure(501)
|
||||
e._curio_pending = {"candidates": []}
|
||||
Kit.check_quiet(e.skip_empty_curio() and e._curio_pending.is_empty(),
|
||||
e.stop()._pending = {"candidates": []}
|
||||
Kit.check_quiet(e.stop().skip_empty_curio() and e.stop()._pending.is_empty(),
|
||||
"空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕")
|
||||
e._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
Kit.check_quiet(not e.skip_empty_curio(),
|
||||
e.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
Kit.check_quiet(not e.stop().skip_empty_curio(),
|
||||
"空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕")
|
||||
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
lr.set_shop_pool([], Callable(), Callable(), Callable())
|
||||
@@ -69,11 +69,11 @@ func check_v7(a, db) -> void:
|
||||
Kit.check_quiet(false, "V7 前置:冒险启动")
|
||||
return
|
||||
# 构成①:全池含优秀档以上——曝光槽应出 1 格高档(rank≥excellent)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
a._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
_tok("armor", "common"), _tok("heal", "excellent"), _tok("refresh", "excellent"), _tok("ap", "rare")]
|
||||
var hi_seen := false
|
||||
for i in 40:
|
||||
var cands: Array = a._draw_curio_candidates("reward_node")
|
||||
var cands: Array = a.deck().draw_candidates("reward_node")
|
||||
if cands.size() != 3:
|
||||
continue
|
||||
var hi := 0
|
||||
@@ -84,10 +84,10 @@ func check_v7(a, db) -> void:
|
||||
hi_seen = true
|
||||
Kit.check_quiet(hi_seen, "曝光槽:全池含高档时三选一呈现 ≥1 格优秀档以上〔专长批 V7〕")
|
||||
# 构成②:高档子池空——该格回退全池(候选仍 3 格全普通)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
a._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
var ok_fb := true
|
||||
for i in 20:
|
||||
var cands2: Array = a._draw_curio_candidates("reward_node")
|
||||
var cands2: Array = a.deck().draw_candidates("reward_node")
|
||||
if cands2.size() != 3:
|
||||
ok_fb = false
|
||||
break
|
||||
@@ -96,17 +96,17 @@ func check_v7(a, db) -> void:
|
||||
ok_fb = false
|
||||
Kit.check_quiet(ok_fb, "子池空回退全池:全普通池候选恒 3 格普通档〔专长批 V7〕")
|
||||
# 构成③:凭证不进子池也不进全池视图(独立计数体系——只经专长凭证注入出现)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
a._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
_tok("feat_ticket", "excellent")]
|
||||
var ticket_seen := false
|
||||
for i in 30:
|
||||
for t in a._draw_curio_candidates("reward_node"):
|
||||
for t in a.deck().draw_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "feat_ticket":
|
||||
ticket_seen = true
|
||||
Kit.check_quiet(not ticket_seen, "凭证不进抽签池(feat_ticket 只经保底注入通道)〔专长批 V7〕")
|
||||
# 构成④:池空=零候选合法态(同族兜底入口)
|
||||
a._curio_deck = []
|
||||
Kit.check_quiet(a._draw_curio_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕")
|
||||
a._curio.deck = []
|
||||
Kit.check_quiet(a.deck().draw_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕")
|
||||
|
||||
|
||||
func check_v6(b, db) -> void:
|
||||
@@ -114,26 +114,26 @@ func check_v6(b, db) -> void:
|
||||
Kit.check_quiet(false, "V6 前置:冒险启动")
|
||||
return
|
||||
# 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点
|
||||
b._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]
|
||||
var gp_before: int = b.pool_stock("gp")
|
||||
b._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]}
|
||||
Kit.check_quiet(b.pick_curio("gp", "common") and b.pool_stock("gp") == gp_before - 1,
|
||||
b._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]
|
||||
var gp_before: int = b.deck().pool_stock("gp")
|
||||
b.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]}
|
||||
Kit.check_quiet(b.stop().pick_curio("gp", "common") and b.deck().pool_stock("gp") == gp_before - 1,
|
||||
"拿了消耗:pick_curio 后牌库摘牌(选中踢池)〔专长批 V6〕")
|
||||
# 没拿回池:抽候选不摘牌——重复抽签同令牌可再现
|
||||
var deck_size: int = b._curio_deck.size()
|
||||
var deck_size: int = b._curio.deck.size()
|
||||
var seen_again := false
|
||||
for i in 30:
|
||||
var cands: Array = b._draw_curio_candidates("reward_node")
|
||||
if cands.size() == 2 and b._curio_deck.size() == deck_size:
|
||||
var cands: Array = b.deck().draw_candidates("reward_node")
|
||||
if cands.size() == 2 and b._curio.deck.size() == deck_size:
|
||||
seen_again = true
|
||||
Kit.check_quiet(seen_again, "没拿回池:抽签不摘牌(未选候选再次可现,牌库余量不变)〔专长批 V6〕")
|
||||
# 只刷出不计:抽签后 _curio_taken 零变化
|
||||
var taken_before: Dictionary = b._curio_taken.duplicate()
|
||||
var taken_before: Dictionary = b._curio.taken.duplicate()
|
||||
for i in 10:
|
||||
b._draw_curio_candidates("reward_node")
|
||||
b.deck().draw_candidates("reward_node")
|
||||
var unchanged := true
|
||||
for k in taken_before.keys():
|
||||
if int(b._curio_taken.get(k, 0)) != int(taken_before[k]):
|
||||
if int(b._curio.taken.get(k, 0)) != int(taken_before[k]):
|
||||
unchanged = false
|
||||
Kit.check_quiet(unchanged, "只刷出不计:抽签不改已拿账〔专长批 V6〕")
|
||||
|
||||
@@ -145,7 +145,7 @@ func check_v12(m, db) -> void:
|
||||
var tier_n := {"common": 0, "excellent": 0, "rare": 0, "legendary": 0}
|
||||
var total := 0
|
||||
for i in 300:
|
||||
for t in m._draw_curio_candidates("level_clear"):
|
||||
for t in m.deck().draw_candidates("level_clear"):
|
||||
tier_n[String(t.get("tier", ""))] = int(tier_n.get(String(t.get("tier", "")), 0)) + 1
|
||||
total += 1
|
||||
Kit.check_quiet(total == 900, "V12 前置:300 停点 ×3 候选=900 席(实 %d)" % total)
|
||||
@@ -163,26 +163,26 @@ func check_v8(c, db) -> void:
|
||||
# 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段
|
||||
var cap_field_before := int(db.adventure.curio_cap_gp)
|
||||
db.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
|
||||
var gp_tokens: int = c.pool_stock("gp")
|
||||
var gp_tokens: int = c.deck().pool_stock("gp")
|
||||
Kit.check_quiet(gp_tokens > 0 and int(db.adventure.curio_value("gp", "common")) == 20,
|
||||
"改 cap 不动概率位:牌库 gp 令牌 %d 枚在库、件值字段不受影响〔专长批 V8〕" % gp_tokens)
|
||||
# cap 只在拿了才计:刷出不计
|
||||
c._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
for i in 20:
|
||||
c._draw_curio_candidates("reward_node")
|
||||
Kit.check_quiet(bool(c._curio_cap_ok("gp", "")) and int(c._curio_taken.get("gp", 0)) == 0,
|
||||
c.deck().draw_candidates("reward_node")
|
||||
Kit.check_quiet(bool(c._curio.cap_ok("gp", "")) and int(c._curio.taken.get("gp", 0)) == 0,
|
||||
"cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕")
|
||||
# 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动)
|
||||
c._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
|
||||
Kit.check_quiet(c.pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
|
||||
Kit.check_quiet(not bool(c._curio_cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
|
||||
c.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
|
||||
Kit.check_quiet(c.stop().pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
|
||||
Kit.check_quiet(not bool(c._curio.cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
|
||||
var gp_in_deck := 0
|
||||
for t in c._curio_deck:
|
||||
for t in c._curio.deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "gp":
|
||||
gp_in_deck += 1
|
||||
var gp_cand := false
|
||||
for i in 30:
|
||||
for t in c._draw_curio_candidates("reward_node"):
|
||||
for t in c.deck().draw_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "gp":
|
||||
gp_cand = true
|
||||
Kit.check_quiet(gp_in_deck >= 0 and not gp_cand,
|
||||
|
||||
@@ -119,9 +119,9 @@ func _init() -> void:
|
||||
adr5.set_save_manager(save_s.new("user://treasure_effects_p.json"))
|
||||
Kit.check_quiet(adr5.start_adventure(261, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["refresh_practice"]}),
|
||||
"刷新练习:装配冒险启动")
|
||||
Kit.check_quiet(int(adr5.refresh_stock()) == 1 and int(adr5.attune_refresh_left()) == 1,
|
||||
Kit.check_quiet(int(adr5.refresh_bank().refresh_stock()) == 1 and int(adr5.refresh_bank().free_left()) == 1,
|
||||
"刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)")
|
||||
Kit.check_quiet(int(adr5._attune_refresh_x) == 0,
|
||||
Kit.check_quiet(int(adr5.refresh_bank()._refresh_x) == 0,
|
||||
"刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)")
|
||||
|
||||
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
|
||||
@@ -221,7 +221,8 @@ func _init() -> void:
|
||||
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free),
|
||||
"刷新执照基线:未装配=表值(%d)" % int(db.adventure.shop_refresh_free))
|
||||
lv10.set_attunement(["refresh_license"])
|
||||
lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(),
|
||||
{"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2,
|
||||
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
|
||||
|
||||
@@ -271,13 +272,12 @@ func _init() -> void:
|
||||
Kit.check_quiet(ob12b.minesweeper.seal_range_free == false,
|
||||
"信标网络反例:未装配封印可达豁免=缺省 false(零装配零扰动)")
|
||||
|
||||
# ⑬ 真视使用计数链(受理成功恰计一次——RunLedger 单类持账)
|
||||
# ⑬ 真视使用计数链(受理成功恰计一次——主角账单点记录)
|
||||
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
|
||||
var ob13 = orch_s.new(db, rng13)
|
||||
var lg13 = load("res://core/run_ledger.gd").new()
|
||||
lg13.vision_counter = {"used": 0}
|
||||
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "ledger": lg13}),
|
||||
"计数链:装配起战(RunLedger 携带)")
|
||||
var lg13 = load("res://core/gain_journal.gd").new()
|
||||
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
|
||||
"计数链:装配起战(GainJournal 携带)")
|
||||
var inv13 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
|
||||
ob13.treasure_inventory = inv13
|
||||
@@ -285,7 +285,7 @@ func _init() -> void:
|
||||
var tgt13: Vector2i = _first_closed_safe(ob13)
|
||||
if tgt13 != Vector2i(-9, -9):
|
||||
ob13.reward_grant().use_treasure(h13, tgt13)
|
||||
Kit.check_quiet(int(lg13.vision_counter.get("used", 0)) == 1,
|
||||
Kit.check_quiet(lg13.vision_used() == 1,
|
||||
"计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)")
|
||||
|
||||
# ⑭ 装配不可变(V6:冒险全程装配快照三面同源且零写口——
|
||||
@@ -308,7 +308,7 @@ func _init() -> void:
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
|
||||
"feats": ["seal_smith"], "tree_rules": [], "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()}
|
||||
"node_type": "battle", "adv_bucket": {}}
|
||||
var rng15 = load("res://core/rng_service.gd").new(); rng15.set_master_seed(341)
|
||||
var ob15 = orch_s.new(db, rng15)
|
||||
ob15.begin_battle(enc_feats)
|
||||
|
||||
+1387
-1384
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
# 迭代 39 代拍板追认单(2026-10-27)
|
||||
|
||||
接收方:用户会话追认(备案确认——授权发生在卡片审问阶段白名单,本单为批后备案)。
|
||||
|
||||
| # | 卡 | 白名单条目 | 裁量内容 |
|
||||
|---|---|---|---|
|
||||
| 1 | 主卡 | 提交切分与 worktree 命名 | 分支 iter39 单工作区;T0/T1/T2/T3/T4/T5 各一笔提交 |
|
||||
| 2 | 主卡 | 测试改址等价写法 | 直读写已迁字段 30+ 处→stop()/hero() 件测试构造面;bind 句柄每回调新建故以关卡号守卫判重(Callable 相等不可靠) |
|
||||
| 3 | 主卡 | 迁入注释随迁措辞 | 公式/兑付/断点段注释随迁 |
|
||||
| 4 | 主卡 | 单关私有 HeroLedger 内部组织 | rebase_override 重注面+reset_for_single_level 值重置(实例不重建) |
|
||||
| 5 | 主卡(超白名单,呈追认) | ctx 增 adv_bucket 键 | 卡片 ctx 键清单漏列桶传递面——按「构造注入一次给齐」意图补全 |
|
||||
| 6 | 副卡 | 种行/补行内部组织 | treasure_inventory 提冒险级共享(卡片文件结构图 treasure_items.gd 零触碰保持;关卡末清空调用点改选择性保留——「同调行始终留在 treasure_inventory」的行承载前提,玩家可见 C 区投影零变化) |
|
||||
| 7 | 副卡(超白名单,呈追认) | 跨版续玩键值兜底补行 | 契约 3「行集为源」的跨版例外——改前档关卡完成停点行已被关末清空(键在行缺),按键值差额补行=与字典轨续玩逐位等价;新档行集为源差额零操作 |
|
||||
| 8 | 触发器卡 | 新技/状态行定名 | desperate_guard/孤注守势、sneak_ambush/趁隙偷袭、defensive_stance/护身(推荐案采纳) |
|
||||
| 9 | 触发器卡 | provisional 数值 | w1/w2、lte 35、−25、×1.5、CD4、duration 2(同数量级内) |
|
||||
| 10 | 触发器卡 | 断言组织与夹具 | trigger_cond_test 内部组织;V5 对拍以 skill_unlock/round_flow 编排集成承载(迁移断言+事件摘要) |
|
||||
@@ -0,0 +1,23 @@
|
||||
# 迭代 39 反哺点清单(2026-10-27)
|
||||
|
||||
接收方:各 owner 文档(文档批落文依据,用户裁决后执行)。
|
||||
|
||||
| # | 文档 | 关联点 | 改成什么 | 依据 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | design/engineering/architecture/architecture.md | 模块归属表 HeroLedger/CurioStop/AttuneLoadout 三行「设计定稿——实现批建件」标注 | 改「已实装(2026-10-27)」;RunLedger 相关行整段退役注记;模块表 level_runner/battle 行消费依赖列 ledger 引用句删除;CurioDeck 行删 bind_handlers 句;level_runner 行补构造注入句;「冒险域拆分定稿」节转现状登记 | 主卡+副卡落地实况 |
|
||||
| 2 | design/gdd/systems/表现与支撑/存档序列化.md | 断点域段机制句(写侧 _bp_parts 拼装/读侧 _bp_read 派生) | 改「各件自交片段(主角账/停点件/关卡件)+冒险应用层唯一拼盘落盘口+获得日记键扩列(gain_journal,28 键——旧档缺键汇总直载)」;同调行「始终留在 treasure_inventory」句随单源化核对 | 主卡 T4/副卡 k4 |
|
||||
| 3 | design/gdd/systems/冒险循环/冒险结构.md | 断点域字段口径 owner——获得日记键字段口径登记 | 键名 gain_journal、缺省策略 plain、旧档回退(缺键=汇总直载跳过比对;在档=明细推导比对不符拒载) | 主卡契约 2 |
|
||||
| 4 | design/gdd/systems/跨局循环/秘宝.md | 充能账机制句若有双轨描述 | 改单源化(物化行唯一事实源、剩余=未用行派生;同调未用行跨关存活——关卡末清空选择性保留) | 副卡落地实况(T0 核该文档若无双轨描述记「无需改」) |
|
||||
| 5 | design/gdd/systems/战斗循环/敌人系统.md v1.2 | §P1 表「情境条件触发器」行「待写卡实装」 | 改「已实装(2026-10-27)」;§半血解锁收编段「字段换代与行值迁移归实装批」句摘除 | 触发器卡落地 |
|
||||
| 6 | design/gdd/systems/战斗循环/战斗结算.md v1.0 | 开轮摇号排除句触发条件指针 | 查实装后无陈述漂移则记「无需改」 | 反哺核对行 |
|
||||
| 7 | design/gdd/systems/冒险循环/难度曲线与Boss.md v1.0 | 技能池投值面 | 查无漂移记「无」;两示范 provisional 投值(w1/w2/lte35/−25/×1.5)登记读表口径 | 触发器卡 §二 |
|
||||
| 8 | design/engineering/architecture/architecture.md | UnitContainer 新公开面(check_trigger_unlocks/事实组包口径)API Boundaries 登记面核查 | 若属登记面补行 | 触发器卡 §六 |
|
||||
| 9 | 体验验证清单.md | 实机目检观测项两条入册 | ①上轮停手→下轮明细出「趁隙偷袭」②地精低血后摇出「孤注守势」 | 触发器卡(登记不阻塞) |
|
||||
| 10 | 待办清单.md | 两源条目状态推进 | 「AdventureRunner 职责真拆分」「技能池情境条件触发器」→完成(batch-closure 承接) | 实现报告 |
|
||||
|
||||
| 11 | design/gdd/systems/跨局循环/生涯.md:55 | 代码行号指路句指 adventure_runner.gd:814(本批收薄后 627 行——行号越界,08d 引用可核新增红) | 行号指路改语义指认(结算受理段已随拆件迁 CurioStop/主角账——按 doc-conventions「引用不转述/代码↔设计追溯由 GDD 承担」改写该句;行号指路本属脆弱面,建议连同 :50/:60 两处留档句一并换代) | 本批 diff 直接后果(commit 被文档域闸拦——反哺落文后本批合回) |
|
||||
|
||||
| 12 | design/engineering/test-conventions.md | 验收尾缀形态:check_quiet 静默收集断言不入 extract_checks.py 扫描面(审计备案);失败消息尾缀对静默通道的字面形态 | 备案级——工具面局限登记(如需覆盖 check_quiet 属提取器改版,工程批) | 独立验收审计备案 |
|
||||
| 13 | 触发器卡 V5 改前基线载体 | 卡定 game/tests/evidence/trigger_baseline.md 未落盘(代拍板 #10 改道 skill_unlock/round_flow 编排集成承载) | 备案——三 Boss 迁移零变化验证强度=语义断言级;后续同类迁移批建议保留事件序列基线落盘义务 | 独立验收审计备案 |
|
||||
|
||||
**边界漏判呈报(副卡契约 3 内在矛盾,已按行留存实现)**:副卡 K4「aux_items 键载库存全量快照(含同调行)」在选路/三选一停点成立、在关卡完成停点不成立(改前行已被关末 reset 清空)——「同调行始终留在 treasure_inventory」与「aux_items 键值形态逐位不变」在该停点形态互斥。实现采「行留存」(关卡末清空改选择性保留+treasure_inventory 提冒险级共享),断点档关卡完成停点 aux_items 含同调未用行(24 停点形态变化,充能读数 attunement_charges 全部逐位一致)。建议 §2 反哺时把该边界写明。
|
||||
@@ -0,0 +1,47 @@
|
||||
# 迭代 39 实现报告(2026-10-27)
|
||||
|
||||
接收方:待办清单「AdventureRunner 职责真拆分」「技能池情境条件触发器」两条目(batch-closure 收尾依据)+本报告 §四呈报项待用户裁决。
|
||||
|
||||
## 一、各卡状态
|
||||
|
||||
| 卡 | 状态 | 收口提交 |
|
||||
|---|---|---|
|
||||
| 主卡·冒险域拆件与账本实体化批 | 完成(T0-T5,wr1-wr8 落地) | 0229d5fb/8d7b12e0/4a9e4300/4da7f3e2/fa1409f4/659d59ed |
|
||||
| 副卡·同调配装充能域封装批 | 完成(RunLedger 整类退役) | 80fe4b8f+收尾笔 |
|
||||
| 触发器卡·技能池情境条件触发器批 | 完成(机制+三 Boss 迁移+两示范) | 4868e11b |
|
||||
|
||||
## 二、实际改动—验收对照表(结果格随附)
|
||||
|
||||
| 需求点 | 验收测试 | 真实入口 | 撤销证明 | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| wr1 账本实体化 | hero_ledger_test hr1/hr1-s/hr2 | start_adventure 真链+商店购买真链 | 回退 shop_session gain_bonus hunk→hr1-s 红→还原复绿(本批执行,undo1) | 绿 |
|
||||
| wr2/wr3 停点件+纯池化 | adventure_feats〔拆件批 V2〕/level_shop_economy〔V3〕现役断言承继 | 三选一真链/pool 购买真链 | (同族 wr1 承载——停点迁移 bp 对拍五时点零漂移主证) | 绿 |
|
||||
| wr4/wr5 信号直连+构造注入 | hero_ledger_test sc3 源码断言(_bind_shop 单点/_forward_ 零命中)+adventure_feats 信号序 | 两关全流程驱动 | (bp 对拍承载) | 绿 |
|
||||
| wr6 断点片段化+日记键 | bp1 对拍 292dumps×27 键六时点逐位零漂移+gain_journal 单列在档+breakpoint_resume P8 28 键 | 同种子双跑 | (bp 对拍主证) | 绿 |
|
||||
| wr7 转发属性撤除 | 改址后现役断言全绿+S13 读数(§三) | 五单测套+ui_input_probe | —— | 绿 |
|
||||
| wr8 产出物 | 本报告三件套 | —— | 缺项红 | 在案 |
|
||||
| 副卡 wr1-wr3 充能单源化 | feat_vouchers 账本保留〔卡② R1〕+al2 单源对拍(attunement_charges 24 停点逐位一致) | 种行真链 | 回退 loadout 种行 hunk→feat_vouchers 红→还原复绿(undo2) | 绿 |
|
||||
| 触发器 wr1-wr9 | trigger_cond_test V1/V2/V4/V6/V7/V8/V9/V10(manifest 登记) | db.load_tables/spawn 真实表/编排器真链 | 回退 rollable_skills 过滤 hunk→V2 红→还原复绿(undo3) | 绿 |
|
||||
|
||||
测试削弱申报履行:主卡契约 9 零削弱;触发器卡申报①②③④已履行(skill_unlock_test 读值源换代语义保持/round_flow expect_pools 期望值随投放改写仍精确比对/:654 直调单参保留/V4 扩行只增不删)。
|
||||
|
||||
## 三、S13 读数与呈报(需用户裁决——见 §四)
|
||||
|
||||
三宿主改善达成:adventure_runner 类变量 42→23(-19)公开方法 23→14(≤20 零警告);level_runner 38→28(卡目标约 30);board_view 45→32(精确达标)。基线文件 `tools/gdstyle_baseline.txt` 零改动。
|
||||
|
||||
**存量红面(收口须用户特批,同 iter38 先例)**:
|
||||
- NEW 5 条:curio_stop max-class-variables=16/max-public-methods=31/max-branches|_apply_curio_pick=10/max-returns=9、hero_ledger max-public-methods=29——新件公开面为卡片接口块定义的迁移语义面(原基线 adventure_runner 对 _apply_curio_pick 同函数登记同值 branches=10/returns=9,超标形态随迁移显现)。
|
||||
- WORSENED 4 条:battle_orchestrator 类变量 50→51(+_hero 拆件必要成员)/文件长 1302→1315/begin_battle 函数长 141→142;level_map_screen 类变量 21→22(iter38 存量漏登记——本批零触碰该文件)。
|
||||
- 建议案:经批准将上列 9 键按实测值 refresh 进基线(含 level_map_screen iter38 存量补登)。
|
||||
|
||||
**EV 锚预期红(触发器卡 T4-4 预设停机呈报)**:round_flow EV 抽样对锚 goblin 实测 0.9148 ∉ [1.00,1.20]——两示范投放=池权重数值改动(孤注守势 buff 技稀释伤害期望,方向合理);钩爪恐魔同批投放。**重锚归用户档位发起,实现批不自行重锚**。
|
||||
|
||||
## 四、遗留读数义务与实机目检总清单
|
||||
|
||||
- 实机目检两项(登记不阻塞):①上轮停手→下轮结算明细出现「趁隙偷袭」;②地精低血后摇出「孤注守势」。
|
||||
- MC 对拍:本批触发器卡含池权重投放=数值改动——MC 锚重锚归用户发起(见 §三)。
|
||||
- 主卡 MC 口径:行为零变化重构(bp1 六时点 292 停点逐位零漂移为凭),MC 未发起。
|
||||
|
||||
## 五、中间产物退役清单(批末执行)
|
||||
|
||||
game/tests/bp_baseline_dump.gd(bp1 载体)+reports/iter39-素材/ 六份 JSON(对拍凭据,审计后随本报告退役)。
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,21 @@
|
||||
# 迭代 39 独立验收审计报告(2026-10-27)
|
||||
|
||||
审计入口:独立子代理(未参与实现);基线=merge-base 5699192af 三点 diff。依据 test-conventions §8。
|
||||
|
||||
## 一、对照表逐行核对——通过(附 1 缺漏已补+2 例外)
|
||||
抽查 8 行全数在案且真实入口无绕过:hr1(直构逐值+start_adventure 真链 :20-52)/hr1-s(商店购买真链 :54-69,正钉 wr1 改动点)/hr2(三通道+片段往返 :71-122)/sc3(_bind_shop 单点+_forward_ 零命中 :124-140)/〔拆件批 V2〕/〔V3〕承继断言/触发器 V1 七反例+V2 双面+V8 阈值式/P8 28 键断言。缺漏 1(V3 保底不逼断言未落码)已补(trigger_cond_test V3 段:special_skills 双态+SkillLottery 30 摇零逼出+撤销态红证)。例外:hr 系尾缀落 manifest 注记行(备案,反哺点 12)。
|
||||
|
||||
## 二、撤销证明抽查复现——通过
|
||||
undo1 复现:基线绿→gain_bonus 行改 pass→hr1-s 恰红(断言钉在被撤改动上)→还原复绿,工作树 clean。undo2/undo3 记录面与码面对得上(抽查一条合规)。
|
||||
|
||||
## 三、测试削弱检查——通过
|
||||
删 72 行 Kit 断言全部属改址(旧路径引用已删字段),逐条同判据替换;净增 1 行=round_flow V7 扩行(申报④兑现);EV 锚区间零触碰(预期红非削绿);申报③兑现;表迁移与申报逐字一致。残留 1(level_shop_economy:1035 RunLedger 注释)已清+al4 零引用源码断言补落。申报外等价改写 1 处(skill_unlock 零扰动/确定性载体换蜘蛛首遇带——投放致地精池含 trigger 行,50f9caa7 已申报,判例外)。
|
||||
|
||||
## 四、S13 基线核查——通过
|
||||
tools/ 全目录零改动(基线禁改契约兑现);存量红 9 键走用户特批呈报(实现报告 §三)。
|
||||
|
||||
## 五、bp 基线对拍抽验——通过
|
||||
before vs after_T5:292×27 键=7884 次逐值比对 mismatch 0;gain_journal 0/292→292/292 在档(契约 2 兑现)。before vs after_attune:attunement_charges 292/292 全等(单源对拍达成);aux_items 24 处=申报例外(关卡完成停点留存同调行,代拍板 #6/反哺点呈报吻合)。
|
||||
|
||||
## 结论
|
||||
**通过(附例外)**——合并前三件已全部处理(V3 断言补落/RunLedger 残留清+al4 补/备案两行入反哺点 12-13)。收口待用户裁决三项:S13 特批(9 键 refresh)、EV 锚重锚(goblin/hook_horror 池权重投放)、08d 反哺落文(生涯.md:55 行号句)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 需求卡片:冒险域拆件与账本实体化批(AdventureRunner 真拆分·主卡)
|
||||
|
||||
- 版本:**v1.2**(v1.0→grilling 一轮「回炉」13 阻断全折入→v1.1→二轮「通过附残留 8 条」全折入→v1.2 定稿,复核记录见 §12)
|
||||
- 版本:**v1.3**(v1.0→grilling 一轮「回炉」13 阻断全折入→v1.1→二轮「通过附残留 8 条」全折入→v1.2 定稿→v1.3 领域拆分修订,复核记录见 §12。v1.3=2026-10-27 领域拆分修订——用户裁结构指标=缺陷,闸 09「标准是尺」口径下的还债实现,用户令本会话讨论后修订)
|
||||
- 来源:[待办清单](../待办清单.md)「AdventureRunner 职责真拆分」条目(2026-10-25 登记,P0,状态=文档设计已闭合+待写卡)(唯一事实源:[architecture](../design/engineering/architecture/architecture.md) v1.16 核心层 HeroLedger/CurioStop 新件行+「冒险域拆分定稿」节)
|
||||
- 归属:`game/core/` 新二件+adventure_runner/level_runner/curio_deck/shop_session/run_ledger/battle_orchestrator 改造+呈现接线面+测量面改址
|
||||
- 排期:未编入(迭代批卡片;估时 ≈5–5.5 个工作日;姊妹副卡另 ≈1.5–2)
|
||||
@@ -32,7 +32,7 @@
|
||||
| u9 | RunLedger 瘦身 | bonus 账/vision 账/adventure_counters 桶全部退役迁出(桶归宿主+encounter `"adv_bucket"` 键);attune_charges 账保留(副卡退役) |
|
||||
|
||||
**完成定义**:
|
||||
1. u1–u9 落地;全套闸按本批改动域选档全绿;S13 计数读数改善(adventure_runner 类变量 42→约 20、level_runner 38→约 30、board_view 45→32——读数记录进实现报告,基线禁改)。
|
||||
1. u1–u9 落地;全套闸按本批改动域选档全绿;S13 计数读数改善(adventure_runner 类变量 42→约 20、level_runner 38→约 30、board_view 45→32——读数记录进实现报告,基线禁改)。**v1.3 增**:拆分后新件逐件落 gdstyle 默认线内——HeroStats 公开方法 12/GpWallet 5/GainJournal 13/RefreshBank 13(类变量各 ≤7)、CurioStop 收口公开方法恰 20、adventure_runner 公开方法 ≤23 基线内;battle_orchestrator 指标精确回位=类变量 50/begin_battle 函数长 ≤141/文件长恰 1314(闸 09 存量 FAIL 行「1314>1302」文本逐字保持,指纹 91d45583cf6d 不变);hero_ledger.gd 删除、grep `HeroLedger|hero_ledger|\.hero()` 代码引用零残留(manifest 文件名与历史性注释除外)。
|
||||
2. 同种子改前改后:断点快照 27 键逐键逐位一致(含日记键前形态);冒烟/系统/单测/探针全绿。
|
||||
3. 撤销证明逐 wr 行落地;三份产出物写齐(T6);独立验收审计过。
|
||||
|
||||
@@ -109,14 +109,18 @@
|
||||
|
||||
| 文件 | 职责 | 行为改动 |
|
||||
|---|---|---|
|
||||
| `game/core/hero_ledger.gd`(**新增**) | 主角实体账:数值面+获得日记三层+gp 单写原语+公式单点+断点片段 | 新件(承载迁移态,行为不变)→ wr1/wr6 |
|
||||
| `game/core/curio_stop.gd`(**新增**) | 三选一停点件:停点三态/刷新三源账/兑付族/凭证出箱/信号面/断点片段 | 新件(承载迁移态)→ wr2/wr3/wr6 |
|
||||
| `game/core/adventure_runner.gd`(修改) | 薄应用层:删数值面/停点族/六转发信号族;建件接线;断点拼盘口;件出口 hero()/stop() | 结构改动,行为不变 → wr4/wr6 |
|
||||
| `game/core/level_runner.gd`(修改) | 构造注入收参;8 白盒属性删;重绑机制删;战斗→关卡信号族保留;断点关卡域片段方法 | 结构改动 → wr5/wr6 |
|
||||
| `game/core/hero_stats.gd`(**新增**,v1.3 拆分) | 主角数值账:生命与特性持有集+永久强化快照+类型轴+生效公式单点+回血封顶+续玩生命钳制+断点片段(hero_hp/relics 两键) | 新件(hero_ledger 领域拆分——行为不变)→ wr1/wr6 |
|
||||
| `game/core/gp_wallet.gd`(**新增**,v1.3 拆分) | GP 钱包:gain/spend/clear 单写原语+断点片段(gp 一键) | 新件(同上)→ wr1/wr6 |
|
||||
| `game/core/gain_journal.gd`(**新增**,v1.3 拆分) | 加算日记:获得记录三层+统一加算区三通道+购买档数+钥石+真视+断点片段(六键) | 新件(同上)→ wr1/wr6 |
|
||||
| `game/core/hero_ledger.gd`(**删除**,v1.3 拆分) | 原主角实体账整件退役——按领域拆入上三件(grep 零残留) | 结构改动(行为不变)→ wr1/wr6 |
|
||||
| `game/core/refresh_bank.gd`(**新增**,v1.3 拆分) | 刷新账件:X 基数与余量/Y 池/刷新券存量账/付费刷计数与报价公式/免费位消耗序+断点片段(refresh_stock/attune_refresh_y 两键) | 新件(curio_stop 领域拆分——行为不变)→ wr2/wr6 |
|
||||
| `game/core/curio_stop.gd`(**新增**) | 三选一停点件:停点三态/兑付族/谓词/凭证出箱/信号面/断点片段;刷新编排留件(查停点态→bank 支出→重抽重发);deck 纯委托删除(调用方直连 CurioDeck) | 新件(承载迁移态)→ wr2/wr3/wr6 |
|
||||
| `game/core/adventure_runner.gd`(修改) | 薄应用层:删数值面/停点族/六转发信号族;建件接线(stats/wallet/journal/bank 四引用);断点拼盘口(三片段并集);件出口 stats()/wallet()/journal()/deck()/refresh_bank()/stop() | 结构改动,行为不变 → wr4/wr6 |
|
||||
| `game/core/level_runner.gd`(修改) | 构造注入收参(ctx 三键 stats/wallet/journal);8 白盒属性删;重绑机制删;战斗→关卡信号族保留;断点关卡域片段方法 | 结构改动 → wr5/wr6 |
|
||||
| `game/core/curio_deck.gd`(修改) | 纯池化:bind_handlers 删;consume_pool_purchase 改经注入 Callable | 结构改动 → wr3 |
|
||||
| `game/core/shop_session.gd`(修改) | 账本引用换代(run_ledger→hero);商店属性加算改经 hero 单写原语 | 结构改动,写通道语义不变 → wr1 |
|
||||
| `game/core/shop_session.gd`(修改) | 账本引用换代(run_ledger→wallet+journal);商店属性加算改经 journal 单写原语 | 结构改动,写通道语义不变 → wr1 |
|
||||
| `game/core/run_ledger.gd`(修改) | 瘦身:bonus 账/vision 账/adventure_counters 桶全部退役迁出(桶归宿主字典+encounter `"adv_bucket"` 键);attune_charges 账+其三原语保留 | 结构改动 → wr1 |
|
||||
| `game/core/battle_orchestrator.gd`(修改) | encounter 注入面:+`"hero"` 键;bonus_add_axis/vision_count_add 调用改指 hero;attune_charge_* 仍走 ledger 瘦壳 | 结构改动 → wr1 |
|
||||
| `game/core/battle_orchestrator.gd`(修改) | encounter 注入面:+`"journal"` 键(v1.3——原 `"hero"` 键随拆分换代);bonus_add_axis/vision_count_add 调用改指 journal;attune_charge_* 仍走 ledger 瘦壳;结构指标回位(类变量 50/函数长/文件长——闸 09 口径) | 结构改动 → wr1 |
|
||||
| `game/presentation/main_adventure_flow.gd`(修改) | 信号重连(level_started 重取点 is_connected 守卫);控制器件注入换代;bonus 读面改指 hero(:37-38) | 结构改动 → wr4/wr1 |
|
||||
| `game/presentation/views/level_map_screen.gd`(修改) | bonus 读面改指 hero(:334/335/343) | 结构改动 → wr1 |
|
||||
| `game/presentation/hud_presenter.gd`(修改) | bonus 读面改指 hero(:20/313) | 结构改动 → wr1 |
|
||||
@@ -134,73 +138,107 @@
|
||||
|
||||
### 接口块
|
||||
|
||||
**HeroLedger(`game/core/hero_ledger.gd`)**
|
||||
**HeroStats(`game/core/hero_stats.gd`,v1.3 拆分——数值域)**
|
||||
|
||||
```gdscript
|
||||
class_name HeroLedger extends RefCounted
|
||||
func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void # 锻造强化快照构造注入(原 _base_override 迁入)
|
||||
class_name HeroStats extends RefCounted
|
||||
func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void # 锻造强化快照构造注入
|
||||
func set_debug_override(atk_pct: int, hp_pct: int) -> void # 调试百分比通道(adventure.set_hero_override 转发件内)
|
||||
# ---- 数值面(自 adventure_runner 迁入,语义逐字不变)----
|
||||
var hero_hp: int # 生命账(原 ledger["hero_hp"] 单值化——共享字典退役)
|
||||
var gp: int # GP 钱包(原 ledger["gp"]——LevelRunner.gp 属性与 ShopSession 钱包读写全改指此单点)
|
||||
func gain_gp(amount: int) -> void # gp 单写原语(clamp ≥0)
|
||||
func spend_gp(amount: int) -> bool # gp 单写原语(不足 false 零副作用)
|
||||
func clear_wallet() -> void # 收束清零
|
||||
func reset_for_single_level() -> void # 单关模式重开重置(值重置非实例重建——shop 绑 hero 对象引用不换代,重绑因此消失;语义=现 start_level :151-164 重置面)
|
||||
func effective_max_hp() -> int # 公式单点(与现 :305-307 逐字同式:表值+base_override+汇总,后乘本件 pct)
|
||||
func effective_atk() -> int # 公式单点(与现 level :101-103 同式,迁入)
|
||||
func effective_ac() -> int # 同上(level :114-115)
|
||||
static func rest_heal(pct: int, hp: int, max_hp: int) -> int # 回血公式单点(自 LevelRunner.rest_heal 迁入)
|
||||
# ---- 获得日记三层 ----
|
||||
func record_gain(source_class: String, kind: String, detail: Dictionary) -> int # 获得记录列表追加,返回记录 id
|
||||
func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void # 明细+汇总同步维护(三通道统一入口;战末通道 source_class="battle_export"、kind=轴名——battle :1125-1127 按轴逐条调)
|
||||
func bonus_total(axis: String) -> int # 汇总读面
|
||||
func record_purchase(product_id: String) -> void # 购买条目(定价读数走 purchase_tier)
|
||||
func purchase_tier(product_id: String) -> int
|
||||
func record_keystone_drop(drop: Dictionary) -> void # 本局增量(keystone 更名 keystone_gained 语义)+逐杀明细
|
||||
func keystone_gained() -> int
|
||||
func record_vision_use() -> void; func vision_used() -> int
|
||||
var feats: Array[String] # 特性持有集(活引用注入——G12 语义)
|
||||
func clamp_hp_to_resume_cap(db_max_hp: int) -> void # 续玩生命钳制(原 _resume_params mini 式逐字收进——上限=表值+永久强化 flat 后乘调试 pct)
|
||||
func rebase_override(raw: Dictionary) -> void
|
||||
func reset_for_single_level() -> void # 单关重开:override 清零+本域账面重置(钱包/日记清零各归各件)
|
||||
func reset_adventure_accounts() -> void # 生命置满生效上限+特性持有集清零
|
||||
func effective_max_hp() -> int; func effective_atk() -> int; func effective_ac() -> int # 公式单点
|
||||
static func rest_heal(pct: int, hp: int, max_hp: int) -> int # 回血公式单点
|
||||
var hero_hp: int # 生命账
|
||||
var feats: Array[String] # 特性持有集(活引用注入)
|
||||
func feats_held() -> Array
|
||||
# ---- 断点片段 ----
|
||||
func breakpoint_fragment() -> Dictionary # hero_hp/bonus(汇总快照)/获得日记键/purchased/keystone 相关/relics/vision_counter_used
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void # 日记缺键→汇总直载跳过比对;在档→推导比对不符 push_error+拒载
|
||||
var attack_type := ""; var armor_type := "" # 两根类型轴快照
|
||||
func breakpoint_fragment() -> Dictionary # hero_hp/relics 两键
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void
|
||||
```
|
||||
|
||||
**CurioStop(`game/core/curio_stop.gd`)**
|
||||
**GpWallet(`game/core/gp_wallet.gd`,v1.3 拆分——钱包域)**
|
||||
|
||||
```gdscript
|
||||
class_name GpWallet extends RefCounted
|
||||
var gp: int
|
||||
func gain_gp(amount: int) -> void; func spend_gp(amount: int) -> bool; func clear_wallet() -> void
|
||||
func breakpoint_fragment() -> Dictionary # gp 一键
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void
|
||||
```
|
||||
|
||||
**GainJournal(`game/core/gain_journal.gd`,v1.3 拆分——加算日记域)**
|
||||
|
||||
```gdscript
|
||||
class_name GainJournal extends RefCounted
|
||||
const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"]
|
||||
func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void # 三通道统一入口(明细+汇总同拍)
|
||||
func bonus_total(axis: String) -> int; func bonus_snapshot() -> Dictionary
|
||||
func record_purchase(product_id: String) -> void; func purchase_tier(product_id: String) -> int
|
||||
func record_keystone_drop(drop: Dictionary) -> void; func keystone_gained() -> int; func keystone_drop_log() -> Array
|
||||
func record_vision_use() -> void; func vision_used() -> int
|
||||
func reset_adventure_accounts() -> void # 日记域账面清零(汇总/明细/记录/已购档/钥石/真视)
|
||||
func breakpoint_fragment() -> Dictionary # bonus/shop_purchased/keystone/keystone_drops/vision_counter_used/gain_journal 六键
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void # 缺日记→汇总直载 clamp;在档→明细推导比对不符 push_error 拒载
|
||||
```
|
||||
|
||||
**RefreshBank(`game/core/refresh_bank.gd`,v1.3 拆分——刷新账域,零停点态依赖)**
|
||||
|
||||
```gdscript
|
||||
class_name RefreshBank extends RefCounted
|
||||
func _init(db_ref: ConfigDb) -> void
|
||||
func set_refresh_x_cap(total: int) -> void # X 基数注入(装配集 bonus_refresh magnitude 合计——宿主 start/resume 时点)
|
||||
func set_refresh_y(v: int) -> void; func refresh_y() -> int # Y 冒险周期池
|
||||
func add_refresh_stock(n: int) -> void; func refresh_stock() -> int # 刷新券存量账(冒险级)
|
||||
func free_left() -> int # 合计读面(X+Y+存量——原 attune_refresh_left 迁入)
|
||||
func spend_free() -> bool # 免费位消耗(X→Y→存量序逐字;全空 false 零副作用)
|
||||
func charge_paid() -> void; func gp_cost() -> int # 付费刷计数递增/报价公式(ceil 基价×曲线^已付费刷——原式迁入)
|
||||
func reset_x() -> void # X 每碰三选一重置回基数(不结转)
|
||||
func reset_paid() -> void # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
func breakpoint_fragment() -> Dictionary # refresh_stock/attune_refresh_y 两键
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void # Y 缺键回退参数面(params_fallback 逐字)
|
||||
```
|
||||
|
||||
**CurioStop(`game/core/curio_stop.gd`,v1.3 收口后保留面)**
|
||||
|
||||
```gdscript
|
||||
class_name CurioStop extends RefCounted # 生命周期=每冒险一次(AdventureRunner 冒险开始建;关卡换实例不换件)
|
||||
signal curio_choice_presented(payload: Dictionary) # 自 adventure_runner 迁入,载荷逐字不变
|
||||
signal curio_choice_presented(payload: Dictionary) # 载荷逐字不变
|
||||
signal curio_resolved(result: Dictionary)
|
||||
func _init(db_ref: ConfigDb, deck_ref: CurioDeck, hero_ref: HeroLedger, snapshot_call: Callable) -> void
|
||||
func rebind_level_runner(runner: LevelRunner) -> void # 每关注入时点=AdventureRunner._start_current_level 建件接线处:存当前关卡对象的 grant_curio_gp 方法 Callable(GP 件入账维持现路径与 wallet_changed 发射序——第二关起绑新 runner,防调死对象)
|
||||
# ---- 五组入口 ----
|
||||
func open_level_clear(payload_base: Dictionary) -> void # 收编 _on_level_finished 停点段:抽签(level_clear)/置在途态/发信号/触发落盘回调
|
||||
func present_node_curio(candidates: Array) -> void
|
||||
func _init(db_ref: ConfigDb, deps: Dictionary, snapshot_call: Callable) -> void
|
||||
# deps = {deck:CurioDeck, stats:HeroStats, journal:GainJournal, bank:RefreshBank}——构造注入一次给齐
|
||||
func rebind_level_runner(runner: LevelRunner, level_index: int) -> void # 每关注入时点(GP 件入账回调换代)
|
||||
# ---- 停点编排保留面(公开方法恰 20——gdstyle 默认线)----
|
||||
func open_level_clear(payload_base: Dictionary) -> void
|
||||
func present_node_curio(candidates: Array) -> void; func node_curio_candidates() -> Dictionary
|
||||
func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool
|
||||
func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool
|
||||
func skip_empty_curio() -> bool
|
||||
func refresh_curio() -> bool; func refresh_curio_paid() -> bool
|
||||
func curio_refresh_gp_cost() -> int; func attune_refresh_left() -> int; func refresh_stock() -> int
|
||||
func curio_stop_pending() -> bool # 名继承现役 :584 谓词
|
||||
func stop_resolved() -> bool # 放行谓词(已兑付待推进且无在途停点)
|
||||
func node_curio_pending_state() -> Dictionary
|
||||
func skip_empty_curio() -> bool; func node_curio_pending_state() -> Dictionary
|
||||
func refresh_curio() -> bool; func refresh_curio_paid() -> bool # 编排=查停点态→bank 支出→重抽重发
|
||||
func curio_stop_pending() -> bool; func stop_resolved() -> bool
|
||||
func consume_pool_purchase(kind: String, tier: String) -> bool
|
||||
func reset_adventure_state() -> void
|
||||
func restore_pending(bp_pending: Dictionary) -> void
|
||||
func restore_resolved(payload: Dictionary) -> void # v1.3 合并 reemit——置态后重发(resume 只重发不重兑付)
|
||||
func clear_resolved() -> void
|
||||
func voucher_outbox() -> Dictionary # 凭证出箱 {rap:int, time_sand:int}——战首取走清零
|
||||
func draw_candidates(channel: String) -> Array # 经 deck(reward_pick 流不变)
|
||||
# ---- 断点片段 ----
|
||||
func breakpoint_fragment() -> Dictionary # bonus_pending/bonus_result/feat_ticket_*/refresh_stock/attune_refresh_y(读侧 params 回退经参传入)
|
||||
func breakpoint_fragment() -> Dictionary # bonus_pending/bonus_result/feat_ticket_*
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void
|
||||
# v1.3 删除:feat_ticket_consider/curio_cap_ok/pool_stock(deck 纯委托——调用方直连 CurioDeck)、
|
||||
# pool_stock_snapshot/draw_candidates(同上)、resolved_payload(零消费死面)、reemit_resolved(并入 restore_resolved)、
|
||||
# 刷新族七方法(set_refresh_x_cap/set_refresh_y/refresh_y/add_refresh_stock/refresh_stock/attune_refresh_left/curio_refresh_gp_cost→RefreshBank)
|
||||
```
|
||||
|
||||
**adventure_runner 应用层(保留/新增面)**
|
||||
|
||||
```gdscript
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, save_manager_ref = null) -> void # 存档承载改构造注入(缺省 null=断点静默,原语义)
|
||||
func hero() -> HeroLedger # 件出口(组件交出,非委托壳)
|
||||
func stats() -> HeroStats; func wallet() -> GpWallet; func journal() -> GainJournal # 件出口(v1.3——hero() 随三件拆分换代)
|
||||
func deck() -> CurioDeck; func refresh_bank() -> RefreshBank # 件出口(v1.3——停点件 deck 纯委托删除后调用方直连面)
|
||||
func stop() -> CurioStop
|
||||
# 保留:start_adventure/resume_adventure/continue_to_next_level/abandon/state/summary/runner()/set_hero_override
|
||||
# 删除:数值面方法族/停点族方法/六转发信号与 _forward_*/_bp_parts 五连挖(改收片段)
|
||||
# 删除:数值面方法族/停点族方法/六转发信号与 _forward_*/_bp_parts 五连挖(改收片段)/hero()
|
||||
# BREAKPOINT_FIELDS 常量保留本件=键序契约(拼盘口按此断言完整性)
|
||||
```
|
||||
|
||||
@@ -208,13 +246,15 @@ func stop() -> CurioStop
|
||||
|
||||
```gdscript
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary) -> void
|
||||
# ctx 键(一次给齐,后置 setter 群删除——grilling 一轮补全 12 键):
|
||||
# hero:HeroLedger/ledger:RunLedger(瘦壳——本卡仅传,副卡删)/
|
||||
# attunement:Array/tree_rules:Array/shop_mode:String/treasure_unlocked:Dictionary/
|
||||
# ctx 键(一次给齐,后置 setter 群删除——grilling 一轮补全 12 键;v1.3 hero 键换代三键):
|
||||
# stats:HeroStats/wallet:GpWallet/journal:GainJournal(共享主角账三件——活引用)/loadout:AttuneLoadout(同调配装件)/
|
||||
# adv_bucket:Dictionary(冒险桶宿主字典)/attunement:Array/tree_rules:Array/shop_mode:String/treasure_unlocked:Dictionary/
|
||||
# hero_attack_type:String/hero_armor_type:String/voucher_rap:int/voucher_time_sand:int/
|
||||
# pool_wiring:Dictionary(池模式装配 4 参收编:{deck:Array, consume:Callable, ticket:Callable, cap:Callable}——缺省空字典=legacy 零注入)
|
||||
# start_level(单关测试模式)件内自建私有 HeroLedger(_init 即建、空 override 注入——shop 绑定因此仍仅构造一次;start_level 收 base_override_in 经 hero 重注+reset_for_single_level() 值重置,实例不重建)
|
||||
# 新增:func hero() -> HeroLedger # 只读出口(bonus 生产读面 5 文件经现 run_ledger 访问链改此承接)
|
||||
# start_level(单关测试模式)件内自建私有三件(_init 即建、空 override 注入——shop 绑定因此仍仅构造一次;
|
||||
# start_level 收 base_override_in 经 stats 重注+三件各归各件清零,实例不重建)
|
||||
# 新增:func stats() -> HeroStats # 只读出口(v1.3——hero() 换代;bonus 生产读面经 journal() 承接)
|
||||
# func journal() -> GainJournal
|
||||
# start_level_external 收参收窄为 (level_index:int, defer_entry:bool, resume_state:Dictionary)
|
||||
# 新增:func breakpoint_fragment() -> Dictionary # 关卡域五键(node_id/visited_ids/aux_items/shop_shelf/shop_visit_buys)
|
||||
# func restore_fragment(resume_state: Dictionary) -> void # 断点重建参数面收口(原 _resume_bp 直读面收编)
|
||||
@@ -327,6 +367,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary) -> void
|
||||
|
||||
**二轮闭合复检(2026-10-26,同一独立子代理)**:判「通过(附残留 8 条,无新阻断)」——13 阻断逐条核闭合(含三处点名矛盾检查:桶迁移与契约 8 一致、rebind 时点先于该关全部信号发射无冲突、副卡键值校验与零行为变化自洽)。残留 8 条已折入 v1.2:R1 数字统一(u8/契约 9/wr7/§6 四处 47→49+B 类 68→78);R2 B 类补 run_ledger 账本断言族(约 10 处/3 文件改指);R3 bonus 读面经 LevelRunner `hero()` 只读出口承接写明;R4 单关模式 `_init` 即建私有 hero(空 override)+start_level 经 reset 重注——绑定仍仅构造一次;R5 结构图 run_ledger 行同步桶退役+battle 直构空字典兜底;R6/R7/R8 归副卡折入。估时分项和 5.35d 对齐。
|
||||
|
||||
**领域拆分修订动因(2026-10-27,v1.3——用户令本会话讨论后修订)**:实现批收口呈报的结构指标超标(hero_ledger 公开方法 29、curio_stop 公开方法 34/类变量 18/_apply_curio_pick 分支 10 返回 9——五条 NEW;宿主 battle_orchestrator 类变量/函数长/文件长三条恶化)经用户裁「结构指标=缺陷、按领域切分」定案为还债实现,随本卡迭代收口批执行:①hero_ledger 按领域拆三件(HeroStats 数值与公式/GpWallet 钱包/GainJournal 加算日记——断点片段三件各交各账、应用层并集拼装,28 键对拍基线逐位一致为硬验收);②curio_stop 拆出 RefreshBank 刷新账件(停点态零依赖)+删五条 deck 纯委托(调用方经 adventure.deck() 直连)+resolved_payload 死面删除+reemit_resolved 并入 restore_resolved;③battle_orchestrator 指标精确回位(类变量 50、begin_battle ≤141、文件长恰 1314——闸 09「标准是尺」存量红两行的行文本与指纹逐字保持,多一行少一行都改指纹)。行为零变化契约不变;机检口径仍按 §13——禁为计数保留无效成员,本修订的全部公开面收敛都来自真实调用面。
|
||||
|
||||
## 13. 禁为机检文本保留无效成员(重构类必填条款)
|
||||
|
||||
禁止为迎合机检消息文本(含计数)而保留或新增无效成员/别名;本批撤除的 25 条转发属性即该债务的清偿——不得以「基线计数」「探针便利」等理由保留任何一条或新增同种转发;机检键与卡片契约互斥时实现段一律停卡呈报,不得自造第三条路(code-conventions §1.1 同款)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 需求卡片:同调配装充能域封装批(AdventureRunner 真拆分·副卡)
|
||||
|
||||
- 版本:**v1.2**(v1.0→grilling 一轮「回炉」5 阻断全折入→v1.1→二轮「通过附残留」R6/R7/R8 折入→v1.2 定稿,复核记录见 §12)
|
||||
- 版本:**v1.3**(v1.0→grilling 一轮「回炉」5 阻断全折入→v1.1→二轮「通过附残留」R6/R7/R8 折入→v1.2 定稿→v1.3 领域拆分修订,复核记录见 §12。v1.3=2026-10-27 领域拆分修订——用户裁结构指标=缺陷,闸 09「标准是尺」口径下的还债实现,用户令本会话讨论后修订)
|
||||
- 来源:[待办清单](../待办清单.md)「AdventureRunner 职责真拆分」条目(2026-10-25 登记,P0,状态=文档设计已闭合+待写卡)(唯一事实源:[architecture](../design/engineering/architecture/architecture.md) v1.16 AttuneLoadout 新件行+「冒险域拆分定稿」节充能单源化句)
|
||||
- 归属:`game/core/` 新一件(attune_loadout)+run_ledger 整类退役+battle_orchestrator 使用/重置路径+断点旧档迁移
|
||||
- 排期:未编入(迭代批卡片;估时 ≈1.5–2 个工作日)
|
||||
@@ -10,7 +10,7 @@
|
||||
## 0. 前置依赖与并行性
|
||||
|
||||
- **第〇步核对(2026-10-26 写卡会话)**:来源条目=设计线条目(同主卡),闭合凭据=architecture v1.16,放行。
|
||||
- **前置卡接口取用**:主卡 T1 产出的 HeroLedger 与 encounter `"hero"` 键、T3 产出的 ctx 注入面与「关卡开始信号前零发射」头注契约、T4 产出的片段三接口形态——本卡在同形接口上扩展充能域。
|
||||
- **前置卡接口取用**:主卡 T1 产出的主角账件(v1.3 拆分后=HeroStats/GpWallet/GainJournal 三件)与 encounter `"journal"` 键、T3 产出的 ctx 注入面与「关卡开始信号前零发射」头注契约、T4 产出的片段三接口形态——本卡在同形接口上扩展充能域。
|
||||
- 并行冲突:与主卡串行(硬前置);与「通关口径换代批」(在册 v1.1)真实交叠=**adventure_runner+level_runner 两文件**(该批 C 注释面 adventure_runner 14 处/level_runner 3 处——本卡改其建件接线与 ctx;该批六件清单无 battle_orchestrator)——先后做者 T0 行号复核吸收;其余在册卡零交叠。
|
||||
|
||||
## 1. 目标与完成定义
|
||||
@@ -122,9 +122,9 @@ func restore_fragment(frag: Dictionary, inventory: TreasureItems) -> void # 旧
|
||||
|
||||
**battle_orchestrator 改指**:`begin_battle` encounter 删 `"ledger"` 键——桶挂链走主卡已建的 `"adv_bucket"` 键(:382 改指已完成于主卡 T1,本卡零动作)、`"hero"` 键沿用;encounter 增 `"loadout": AttuneLoadout` 键;`_reseed_attune_charges(reset)` 改调 `loadout.reset_cycle(reset, treasure_inventory)`(:228-246 语义逐字承);使用点 `:757` 改 `loadout.consume_charge(...)`;`:239` 充能设定改种行路径;`:381` ledger 类型检查与 `RunLedger.new()` 兜底删除。
|
||||
|
||||
**测试直构面改址(grilling 一轮补查)**:treasure_effects_test:275、round_flow_skill_pool_test:969/994(`load("res://core/run_ledger.gd").new()` 直构+encounter "ledger" 注入)→ 改直构 HeroLedger+"hero"/"adv_bucket" 键注入;meta_progression_forge_test:290-291 同款;effect_engine.gd:94/effect_context.gd:3 注释中 RunLedger 表述随批清(注释面,非行为)。
|
||||
**测试直构面改址(grilling 一轮补查)**:treasure_effects_test:275、round_flow_skill_pool_test:969/994(`load("res://core/run_ledger.gd").new()` 直构+encounter "ledger" 注入)→ 改直构主角账件+`"journal"`/`"adv_bucket"` 键注入(v1.3——原 `"hero"` 键随主角账三件拆分换代);meta_progression_forge_test:290-291 同款;effect_engine.gd:94/effect_context.gd:3 注释中 RunLedger 表述随批清(注释面,非行为)。
|
||||
|
||||
**主卡接口取用**:HeroLedger(无充能交互)、ctx 注入面(本卡扩两键)、片段三接口形态(本卡同形扩展)。
|
||||
**主卡接口取用**:主角账件(v1.3 拆分后=HeroStats/GpWallet/GainJournal 三件——充能交互零涉及)、ctx 注入面(本卡扩两键)、片段三接口形态(本卡同形扩展)。
|
||||
|
||||
### 落位与可复用性预检
|
||||
|
||||
@@ -202,6 +202,8 @@ func restore_fragment(frag: Dictionary, inventory: TreasureItems) -> void # 旧
|
||||
|
||||
**二轮闭合复检(2026-10-26,同一独立子代理)**:判「通过(附残留,无新阻断)」——5 阻断全核闭合、「键值仅校验读数与零行为变化」边界判自洽。残留 R6/R7/R8 已折入 v1.2:R6 契约 2 旧句(两读法兜底)删、wr1 列改单源对拍、§1 目标句旧措辞(同调行迁移)改「旧档读侧以行集为源」;R7 结构图补 feat_vouchers_test 充能直写 8 处改指行;R8 并行注记事实订正(通关批 C 面六件无 battle_orchestrator,真实交叠=adventure_runner+level_runner)。
|
||||
|
||||
**领域拆分修订动因(2026-10-27,v1.3——用户令本会话讨论后修订)**:主卡 v1.3 领域拆分(用户裁「结构指标=缺陷」还债实现)把主角账 HeroLedger 拆为 HeroStats/GpWallet/GainJournal 三件、battle encounter `"hero"` 键换代 `"journal"`——本卡对主卡产物的取用面(前置卡接口取用句、测试直构改址行、接口取用行)随换代同步改写;本卡自身范围(AttuneLoadout 建件、RunLedger 退役、使用管线逐位不变)零改动。
|
||||
|
||||
## 13. 禁为机检文本保留无效成员(重构类必填条款)
|
||||
|
||||
同主卡:禁止为迎合机检消息文本保留或新增无效成员/别名;RunLedger 的删除不得以任何「基线便利」理由拖延或保留空壳;机检键与契约互斥时停卡呈报(code-conventions §1.1 同款)。
|
||||
@@ -1,23 +1,24 @@
|
||||
# 需求卡片:结构小件定点修批(审计 §三 其余小件六件:死变量/confirm 收口/空判/常驻 flush/fatal 违约回调/未播种取数通道)
|
||||
|
||||
- 版本:**v1.3**(2026-10-26 定稿——含迭代 38 停卡后的载体修订与复核轮收紧;逐轮 grilling 与停卡呈报记录见 §12)
|
||||
- 版本:**v1.4**(2026-10-27 定稿——迭代 40 卡② 施工实测 p5 原处方在 `RewardEconomy` 上顶破类变量阈值,用户裁「缝拆+另立卡」替换 p5 处方并加两条验收;v1.3 的验收载体改独立负面套件与 p7 并修沿用;逐轮 grilling 与停卡呈报记录见 §12)
|
||||
- 来源:[待办清单](../待办清单.md)「结构审查未立项小件候审」条目(2026-10-25 登记,状态=待裁决)——**2026-10-26 用户裁「全六件立项」**;本卡收六件(①③④⑤⑥ 审计原编号+p7 同函数第二处,2026-10-26 用户裁「并修」随 v1.3 入卡),件②(level_map 越界/BFS)独立成卡、已随迭代 38 卡① 合回退役(git 提交 b85b3b45/8ea7cfba)(唯一事实源:工程证据指认=审计报告 [§三 其余小件](审计-game代码结构审查-2026-10-24.md) §90-95 逐件行号+本卡 §3 行号级实查〔迭代 37 拆件后行号重定位〕)
|
||||
- 归属:`game/presentation/main_overlay_flow.gd`+`game/presentation/main.gd`+`game/presentation/row_pacer.gd`(消费一行)+`game/core/board_generator.gd`+`game/core/reward_economy.gd`+`game/core/rng_service.gd`+`game/core/battle_orchestrator.gd`(组装接线两行)+测试面(新增负面套件一件+其 `.uid` 边车+`game/tests/run_all.gd` manifest 登记一行+探针回归)
|
||||
- 排期:未编入(迭代批卡片;估时 ≈0.85 工作日——含负面套件新件、manifest 登记与清单对账 ≈0.1d;归属车道随「gdstyle 存量债全量清偿」条目波 7 或另编迭代,由编排段定)
|
||||
- 归属:`game/presentation/main_overlay_flow.gd`+`game/presentation/main.gd`+`game/presentation/row_pacer.gd`(消费一行)+`game/core/board_generator.gd`+`game/core/reward_economy.gd`+`game/core/battle_fx_host.gd`(v1.4 追加一枚公开查询口;该件由迭代 40 卡① 建)+`game/core/rng_service.gd`+`game/core/battle_orchestrator.gd`(组装接线:fatal 两行+一行换一行)+测试面(新增负面套件一件+其 `.uid` 边车+`game/tests/run_all.gd` manifest 登记一行+`game/tests/unit/battle_settlement/variant_placement_test.gd` 追加三条断言+探针回归)
|
||||
- 排期:未编入(迭代批卡片;估时 ≈1.0d——v1.3 的 ≈0.85d+v1.4 缝拆增量 ≈0.15d=宿主查询口一枚、掷格件成员替换、三条断言、dump 六组改前基线采集与批末 diff。归属车道随「gdstyle 存量债全量清偿」条目波 7;卡序随迭代 40 行内排列(卡①→卡②),由编排段定)
|
||||
- 开工门:U0+复核门过(§12);零待裁点
|
||||
|
||||
## 0. 前置依赖与并行性
|
||||
|
||||
- **第〇步核对(2026-10-26 写卡会话)**:来源条目=需求性工程条目(审计行号证据),用户 2026-10-26 点名全六件立项——放行。
|
||||
- 审计行号基线=2026-10-24(迭代 37 拆件前);本卡 §3 已按现行 master 重定位(①main→main_overlay_flow 拆件迁移、③行号漂移),**v1.3 复核轮再按迭代 38 合回点 8ea7cfba 重锚一遍**(`battle_orchestrator.gd` 组装点与 `_fatal`、`main.gd` 各址均漂移),开工日 T0 仍须复核。
|
||||
- 审计行号基线=2026-10-24(迭代 37 拆件前);本卡 §3 已按现行 master 重定位(①main→main_overlay_flow 拆件迁移、③行号漂移),**v1.3 复核轮再按迭代 38 合回点 8ea7cfba 重锚一遍**(`battle_orchestrator.gd` 组装点与 `_fatal`、`main.gd` 各址均漂移),**v1.4 的 p5 缝拆面另按迭代 40 卡① 落定形态锚**(见下条硬前置),开工日 T0 仍须复核。
|
||||
- 与 2026-10-25 两批卡零文件交叠(卡 A 触 battle_orchestrator 的发射点/头注/校验段——本卡 p5 只动组装点的两行接线与两协作件 `_fatal` 成员,**按语义面判交叠、不按行号**:行号随合回漂移,「发射与校验段」对「组装点接线行」的面不相交在换代后仍成立;rng_service p6/p7 与其零叠)。同波无冲突。
|
||||
- p6/p7 同触 `rng_service.gd:100-112` 同一函数的两条分支——v1.3 并修后两处改动面相邻,禁拆成两波同文件互踩(一条波次内完成)。
|
||||
- **与「核心数据面结构收敛批」(2026-10-27 v1.0,在册同条目)文件交叠**:同触 `board_generator.gd`(本卡改 `_fatal` :396-398)与 `reward_economy.gd`(本卡改 `_fatal` :366-368)——该卡 §0 已记此交叠并裁「编排串行、后行者 T0 吸收行号漂移」。本卡体量最小(六件皆行级),**宜先行**;若被排在该卡之后,§1/§3/S5 的行号锚一律按其 T0 重核,禁照搬本卡现值。
|
||||
- p5 触结算域(core 件 _fatal 形态)——parity 触面按 MC 纪律:零数值改动=零 MC(_fatal 是违约路径,合法路径零触碰)。
|
||||
- **p5 硬前置=迭代 40 卡①(B1 战斗域拆件批)**:v1.4 缝拆的落点件 `game/core/battle_fx_host.gd`(`BattleFxHost`)由卡① 建件——卡② p5 必在卡① 之后施工(roadmap 迭代 40 行内卡序=执行序);组装点现形 `_reward_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx)`,行号与形态按卡① 落定后 T0 重核,禁照搬本卡 v1.3 的 :285/:290 锚。
|
||||
- **与「核心数据面结构收敛批」(2026-10-27 v1.0,在册同条目)文件交叠**:同触 `board_generator.gd`(本卡改 `_fatal` :396-398)与 `reward_economy.gd`(本卡改 `_fatal` :366-368+v1.4 缝拆 :20-26/:63-69)——该卡 T9 拆的是同文件 `roll_board` 的 :42-52/:220-259 三段,与本卡改段同函数不相交、行号互漂必发;其 §0 已裁「编排串行、后行者 T0 吸收行号漂移」。本卡体量最小,**宜先行**;若被排在其后,§1/§3/S5/S8 的行号锚一律按其 T0 重核,禁照搬本卡现值。
|
||||
- p5 触结算域(两 core 件 `_fatal` 形态+`RewardEconomy` 与 `BattleFxHost` 之间一处成员替换)——parity 触面按 MC 纪律:零数值改动=零 MC(`_fatal` 是违约路径,缝拆只搬判定、权重表与池结构一字不动,合法路径零触碰);行为零变化的兜底由 wr11 的 dump 六组逐位 diff 承担。
|
||||
|
||||
## 1. 目标与完成定义
|
||||
|
||||
**目标**:六件审计定点小件落地——①_ sync_status_entry 死变量与重复四句清除;③settle_batch 两入口显式 null 防护;④row_pacer 会话更替机制化 flush;⑤两 core 协作件 _fatal 的 release 软回退补真中止通道(经 orchestrator 既有封印传导);⑥rng_service open_stream 未知标签补 return+可测违约回调;⑦同函数未播种取数分支(`_master_seed < 0`)同款补 return+违约回调(p6 同病根第二处,v1.3 用户裁并修)。合法路径行为逐位不变(p2 confirm 收口是唯一行为改动=锁域归位,对照表专行)。
|
||||
**目标**:六件审计定点小件落地——①_sync_status_entry 死变量与重复四句清除;③settle_batch 两入口显式 null 防护;④row_pacer 会话更替机制化 flush;⑤两 core 协作件 `_fatal` 的 release 软回退补真中止通道(经 orchestrator 既有封印传导),其中 `RewardEconomy` 同步做效果层缝拆(v1.4:`_fx`/`_ctx` 与 `bias_rewards` 判定搬进 `BattleFxHost`,类变量数不越阈值);⑥rng_service open_stream 未知标签补 return+可测违约回调;⑦同函数未播种取数分支(`_master_seed < 0`)同款补 return+违约回调(p6 同病根第二处,v1.3 用户裁并修)。合法路径行为逐位不变(p2 confirm 收口是唯一行为改动=锁域归位,对照表专行)。
|
||||
|
||||
**验收载体口径**(v1.3 停卡修订立此,实装段禁改回落位):违约路径取证(wr5/wr6/wr7)**一律落独立负面套件**并按 manifest 登记 `expect=1, negative=true`,禁落现役非负面套件——headless 下 `assert` 失败必产 `SCRIPT ERROR` 行、不可捕获,非负面套件的输出被闸 02 扫描命中即判 FAIL(规则出处=test-conventions §5「负面套件仅可豁免第 2 闸,且须在 manifest 显式标记」;在册先例三件=`reward_dangling_treasure_test`/`counter_prefix_fatal_test`/`extended_row_violation_test`)。
|
||||
|
||||
@@ -29,14 +30,14 @@
|
||||
| p2 | confirm 旁路单口违约:`main_overlay_flow.gd:88` `root.confirm_dialog.open(...)` 与 `main.gd:311/:320` `confirm_dialog.close()` 直开直关——绕过 `_open_modal/_close_modal`(main.gd:281-285 → ModalFlow.open/close 挂起兜底/遮挡记账/开侧同步),违 main.gd 自身头注单口纪律;confirm_dialog.gd:40-41「树序由构建序保证」注释失真 | 三处收口:open 前经 `root._open_modal(root.confirm_dialog)`(ModalFlow.open 承载显示编排,T0 核其与 confirm_dialog.open 设文案的先后序);close 经 `root._close_modal(...)`;:40-41 失真注释随修。**行为改动=confirm 开合进 ModalFlow 栈(输入锁/互斥域归位)**——对照表专行+探针回归 | 审计 ①后半;架构单口纪律 |
|
||||
| p3 | `main.gd:721/:748` settle_batch 两入口直取 `run_scope.settlement.minesweeper.settle_batch(...)`——前置空判隐式依赖 `_input_locked` 第三例外(锁谓词保证只在局内可达) | 两入口各加显式防护:`if run_scope.settlement == null: return`(零成本双保险——锁谓词异常时不再崩于空引用) | 审计 ③「入口显式 null 防护零成本双保险」 |
|
||||
| p4 | `row_pacer.gd:11` `_rows` 待播队列住 main 级常驻节点(违架构「局态一律住 RunScope」)——会话更替零残留靠时序约定兜底 | `main.gd:175` `_begin_session` 内加 `_row_pacer.flush()` 一行(:49 现成公开面——机制保证会话更替即清队) | 审计 ④「_begin_session 内 flush 一行变机制保证」 |
|
||||
| p5 | `board_generator.gd:396-398` 与 `reward_economy.gd:366-368` 两处 `_fatal`=push_error+assert(false)——release 模板剥离 assert 后退化为软回退(无 orchestrator 的 battle_result 封印兜底) | 两件 `_fatal` 各加可注入违约回调:`var fatal_handler: Callable`(缺省 `Callable()`=现行为逐字保持)+`_fatal` 体内 handler 有效则调用;组装点 `battle_orchestrator.gd:285/:290` 各接一行 `fatal_handler = _fatal`(orchestrator 自身 _fatal :1304 已带 battle_result 封印——协作件违约即传导封印=战斗中止,release 真中止成立) | 审计 ⑤「出包前需换真中止(可经宿主注入 fatal 回调)」——经组装层传导,零穿透三层、复用既有封印 |
|
||||
| p5 | 两协作件 `_fatal`=push_error+assert(false),release 模板剥 assert 后退化软回退(`board_generator.gd:396-398`/`reward_economy.gd:366-368`);且 `RewardEconomy` 直持效果引擎——`_fx`(:20)、`_ctx`(:21)、`set_fx`(:24-26) 只为 :63-69 一处 `bias_rewards` 求和服役,该件类变量现行 15=gdstyle 阈值顶(基线无该键,第 16 枚即出闸 09 新增红) | **缝拆+fatal 通道,三处**:① `game/core/reward_economy.gd` 删 `_fx`/`_ctx`/`set_fx`,改注入 `var luck_bias_query: Callable`,保留 `var fatal_handler: Callable`(类变量 15−2+2=净 15);② `game/core/battle_fx_host.gd` 新增 `func luck_bias_for(cell_count: int) -> int`,把 `reward_roll` 发射与 `bias_rewards` 求和收回效果层(体内三参形 dispatch,见契约 7);③ `game/core/battle_orchestrator.gd` 组装点 `_reward_economy.set_fx(...)` 一行 → `_reward_economy.luck_bias_query = _fx_host.luck_bias_for`(1 换 1,行数不变),另在两件 new 之后各接一行 `fatal_handler = _fatal` | 审计 ⑤+2026-10-27 用户裁「缝拆+另立卡」;判定搬进已持该引擎的件=一处真迁移,禁为凑计数保留无效成员(契约 5) |
|
||||
| p6 | `rng_service.gd:100-103` `open_stream` 未知标签 push_error+assert 后**无 return**——release 剥 assert 后静默继续发号(违随机流纪律) | 补违约通道:`var violation_handler: Callable`(缺省现行为)+未知标签分支 handler 有效则调用+**`return null`**(合法路径零变化;release 下返回 null 使调用方即刻崩=fail-fast 传导);handler 注入使负路径 debug 可测(见 wr6) | 审计 ⑥「补 return 即闭合」+可测性(撤销证明口径) |
|
||||
|
||||
| p7 | `rng_service.gd:104-106` `open_stream` 的 `_master_seed < 0` 分支=push_error+assert 后**同样无 return**(p6 同病根第二处,grilling 一轮点名、v1.3 用户裁并修)——未播种即取数在 release 剥 assert 后静默继续发号(违 EC7 与随机流纪律) | 未知标签通道同款:该分支复用 p6 的 `violation_handler`(handler 有效则调用,传用途标签)+**`return null`**(合法路径零变化;release 下调用方即刻崩=fail-fast 传导);违约取证落负面套件第 ③ 测试点(见 wr7) | 审计 ⑥同函数第二处;卡面 §9 L3 原停卡条款经用户裁转并修 |
|
||||
|
||||
**完成定义**:
|
||||
|
||||
1. p1–p7 落地;合法路径闸按域选档处置闭合——本批属代码面(`game/` 下非 `.md`),验收=`tools/run_all_gates.sh --code`;**闸 09 现状不是绿**:09 静态扫描基线为迭代 38 带红合并时用户特批登记的存量红(`game/tests/.gates/baseline.json` 闸 09 `exit=1`、标记 `user-approved-iter38`),p5 接线两行加在 `battle_orchestrator.gd`(现 1313 行,基线 `max-file-length` 登记 1302)=同键继续恶化,p6/p7 给 `rng_service.gd` 加成员与两行是否出 NEW 键须实测(`gdstyle_gate.py` 的 SCAN_DIRS=core/data/presentation,不含 `game/tests/`,故新测试文件本身零影响)——**实现段开工前由用户二选一处置:随批还债(压缩该文件行数)或经批准刷新基线,禁以「闸 09 全绿」为完成定义**;其余闸段全绿;
|
||||
1. p1–p7 落地;合法路径闸按域选档处置闭合——本批属代码面(`game/` 下非 `.md`),验收=`tools/run_all_gates.sh --code`;**本卡对闸 09 的义务=零新增**:`tools/gdstyle_baseline.txt` 一字不动(登记值只降不升,禁 `--refresh` 抬登记值换绿),`RewardEconomy` 类变量净 15(实测出第 16 枚=L3 停呈)、组装点 1 换 1 净增零行(两枚 fatal 接线按 T0 实测不得使 `battle_orchestrator` 的 `max-file-length` 较登记 1302 恶化,否则停呈)、p6/p7 给 `RngServiceCore` 加一枚成员实测零 NEW 键;闸 09 现行两条红(`battle_orchestrator` 行数、`level_map_screen` 类变量)属在册存量债,本卡不清零、不登记新值;其余闸段全绿;
|
||||
2. wr 断言绿(负面套件三点=wr5 封印传导+缺省、wr6 未知标签违约通道、wr7 未播种违约通道;加 p2 锁域回归与 wr4 夹具断言);
|
||||
3. 撤销证明四条(p1/p5/p6/p7 各一——§8.1,负面套件那三条按退出码口径判红);三份产出物写齐(T2)。
|
||||
|
||||
@@ -49,8 +50,10 @@
|
||||
| D3 | [code-conventions](../design/engineering/code-conventions.md) §1(死代码/重复语句)+fail-fast 红线 | p1/p3/p5/p6 依据 |
|
||||
| D4 | 2026-10-26 用户令「全六件立项」 | 立项凭据 |
|
||||
| D5 | 2026-10-26 用户裁「把需求变更给修正确」(迭代 38 卡② 停卡呈报后)——批准 v1.3 两处载体修订=wr5/wr6/wr7 落独立负面套件+p7 并修 | 停卡修订凭据 |
|
||||
| D6 | 2026-10-27 用户裁「缝拆+另立卡」(迭代 40 卡② 施工呈报 p5 原处方在 `RewardEconomy` 上顶破类变量阈值后) | p5 处方替换凭据 |
|
||||
| D7 | [code-conventions](../design/engineering/code-conventions.md) §1.1(一类一职责/禁为机检保留无效成员)+§1.3(依赖单向)+§2.1(跨类调用契约成文)+迭代 40 卡① 契约 5(跨件单向接线、禁 RefCounted 环) | v1.4 缝拆合规凭据 |
|
||||
|
||||
## 3. 现状事实(2026-10-26 写卡会话实查,v1.3 复核轮按迭代 38 合回点 8ea7cfba 重锚行号——审计 2026-10-24 行号已按拆件后重定位;开工日 T0 复核行号)
|
||||
## 3. 现状事实(2026-10-26 写卡会话实查,v1.3 复核轮按迭代 38 合回点 8ea7cfba 重锚行号——审计 2026-10-24 行号已按拆件后重定位;v1.4 补 S8 缝拆面实查,p5 落点件面按迭代 40 卡① 落定形态锚;开工日 T0 复核行号)
|
||||
|
||||
| # | 事实 | 出处 |
|
||||
|---|---|---|
|
||||
@@ -61,20 +64,22 @@
|
||||
| S5 | 两协作件 `_fatal`=push_error+assert(false) 两行(无兜底——board_generator 实址 :396-398,grilling 订正 v1.0 的 :391-393;reward_economy :366-368 准确);组装点在 orchestrator(:285 `BoardGenerator.new()`、:290 `RewardEconomy.new(db, rng)`);orchestrator 自身 `_fatal`(:1304)=push_error+`battle_result = "fatal_error"` 封印+assert(release 剥 assert 后封印仍在=唯一阻断面) | `game/core/board_generator.gd:396-398`;`game/core/reward_economy.gd:366-368`;`game/core/battle_orchestrator.gd:285/290/1304-1307` |
|
||||
| S6 | `open_stream` 未知标签分支(:101-103)push_error+assert 后直落 `_master_seed` 检查与发号——无 return;PURPOSE_TAGS 封闭词表(头注 EC12) | `game/core/rng_service.gd:100-112` |
|
||||
| S7 | 同函数 `_master_seed < 0` 分支(:104-106,2026-10-26 v1.3 复核码面仍在)=push_error+`assert(false, "未播种取数致命(EC7)")` 后同样无 return,直落 :107 起的取号与发号段——debug 下 assert 只中断当前函数、控制流回调用方,release 下剥 assert 后静默发号 | `game/core/rng_service.gd:104-112` |
|
||||
| S8 | v1.4 缝拆面实查:`RewardEconomy` 类变量 15=:8-18 十一枚+:20/:21 两枚+`board_skew`(:191)+`_treasure_unlocked`(:306),恰在 gdstyle 阈值顶且基线无该键;`_fx`/`_ctx` 全仓唯一消费点=:66-69 的 `bias_rewards` 求和(`roll_board` 内,开流之后一步);`set_fx` 全仓唯一调用点=`battle_orchestrator.gd:295`,其余五处构造(`sim/reward_expectation.gd`+两测试)从不注入=删该法零外部破坏面;`DamageController`(:46/:47/:71,五枚事件 attack_commit/armor_roll/clamp_check/kill/damage_taken+计数器读写回写 :348/:354) 与 `ActionEconomy`(:44/:45/:50,三枚事件 dig_paid/dig_stop/charge_full) 同款直连——本卡不动,归新登记条目(§5 不做) | `game/core/reward_economy.gd:20-26/63-69/191/306`;`game/core/battle_orchestrator.gd:293-295`;`tools/gdstyle_baseline.txt`(键账实测) |
|
||||
|
||||
## 4. 设计契约(红线,违反 = L3 停机呈报)
|
||||
|
||||
🔴 **契约 1 · 改动面封闭**:p1–p7 只动 §1 所列行;合法路径(无违约输入)行为逐位不变——结算、随机发号、排播时序、角标同步零变化;p2 是唯一申报的行为改动(锁域归位)。
|
||||
🔴 **契约 1 · 改动面封闭**:p1–p7 只动 §1 所列行(v1.4 的 p5 含三处=`RewardEconomy` 成员替换、`BattleFxHost` 追加一枚公开方法、组装点一行换一行);合法路径(无违约输入)行为逐位不变——结算、随机发号、排播时序、角标同步零变化;p2 是唯一申报的行为改动(锁域归位)。
|
||||
|
||||
🔴 **契约 6 · 负面载体不得回落现役套件**(v1.3 停卡修订立):违约取证若被写回现役非负面套件,闸 02 必红且违 test-conventions §5 的显式标记要求——实装段禁自行「复用既有套件省事」。三测试点的回归态判据**只能取「改前改后真有差异」的可观测面**=handler 成员与调用点是否存在(记录位)、封印是否落;`return` 位与计数器读数在 debug 下两态不可判别(assert 先中断函数本体,控制流回调用方,`:107-108` 的推计数段谁也到不了),把它当运行期判据=假绿——p6/p7 的 `return` 走静态判据+grep 在位+评审注记(同 wr3 手法)。
|
||||
|
||||
🔴 **契约 2 · 缺省零变化**:p5/p6 的 handler 缺省(`Callable()`)时两件 `_fatal` 与 `open_stream` 行为逐字等于现状——禁顺手改 push_error 文案与 assert 条件。
|
||||
🔴 **契约 2 · 缺省零变化**:p5/p6/p7 的 handler 缺省(`Callable()`)时两件 `_fatal` 与 `open_stream` 行为逐字等于现状;v1.4 的 `luck_bias_query` 缺省(`Callable()`)时 `luck_bias` 恒 0=现行逐位不变(现网无任何 `bias_rewards` 数据行,S8 实查);禁顺手改 push_error 文案与 assert 条件。
|
||||
|
||||
🔴 **契约 3 · 封印传导不绕**:p5 组装接线后,协作件违约必须经 orchestrator `_fatal` 落 battle_result 封印——禁另立第二中止通道。
|
||||
|
||||
🔴 **契约 4 · 排播语义不变**:p4 的 flush 只在会话更替时点清队——正常运行期排播节奏零变化(pacing_queue_test 回归)。
|
||||
|
||||
🔴 **契约 5 · 禁为机检文本保留无效成员**(req-card 第 13 条,本卡属定点重构类):禁为避免断言/清单更新保留死变量或重复句(p1 歧路拦截);机检键与本卡契约互斥时一律停卡呈报。
|
||||
🔴 **契约 7 · 发射面逐字同形**(v1.4 立):`luck_bias_for` 体内必须走 `engine.dispatch(counters_ctx, "reward_roll", {"polarity": "positive", "cell_count": cell_count})` 的**三参形**,**禁复用宿主 `fx_emit`**——那枚带第四实参 `_fx_fact`(`facts: Callable`),会打开订阅行 `when` 的事实条件读取面、命中集可变=契约 1 破。发射时点(开流之后、`_biased_cell_rows` 装配之前)、事件名、payload 键值三件逐字不变;取号序由 wr11 六组逐位 diff 兜。
|
||||
|
||||
## 5. 范围边界
|
||||
|
||||
@@ -85,6 +90,7 @@
|
||||
- orchestrator 自身 `_fatal` 改造(已有封印,审计未点);
|
||||
- run_all.gd/probe_gate.sh 挂死防护(闸壳件②——工程直改通道,另拆注);
|
||||
- confirm_dialog 的 open/close 方法本体(收口在调用面,方法语义不动)。
|
||||
- `DamageController`/`ActionEconomy` 两处 `_fx`/`_ctx`/`set_fx` 直连的「效果发射端口」化,以及 `BattleOrchestrator` 瘦身(卡① 后仍留 38 类变量,在册豁免登记中)、`RewardEconomy` 掷格面与受理账面分件——三条归待办清单「战斗域结构深拆批」条目(2026-10-27 登记,状态=待文档设计),本卡不裁端口形态、不裁件边界;
|
||||
|
||||
## 6. 关联文档同步面
|
||||
|
||||
@@ -92,7 +98,7 @@
|
||||
|---|---|---|
|
||||
| `待办清单.md` | 来源条目状态:原「结构审查未立项小件候审」六件候审条目 2026-10-26 已并入「gdstyle 存量债全量清偿」条目波 7(本卡为其登记在册的现成卡),本卡出账随该条目波 7 推进,禁另立重复条目 | 反哺点 · 列清单(①:状态归属注记) |
|
||||
| `design/engineering/test-conventions.md` §5 | 负面套件豁免第 2 闸的规则**已在册、零修订预期**——本卡只是首次把「运行期违约通道取证」这条落位规则用满(先例三件在册);如复核认为该规则该升级为「违约通道类需求点必须落负面套件」的显式条款,列反哺点由文档批裁 | 反哺点 · 列清单(③:预期零改动注记) |
|
||||
| [architecture](../design/engineering/architecture/architecture.md) 模块归属表 | p2/p4 若涉契约表述(单口纪律/RunScope 条款现行表述已覆盖——预期零改动;T0 核) | 反哺点 · 列清单(②:预期零改动注记) |
|
||||
| [architecture](../design/engineering/architecture/architecture.md) 模块归属表+「战斗编排域拆件定稿」节 | p2/p4 涉单口纪律与 RunScope 条款(现行表述已覆盖,预期零改动);**v1.4 缝拆三处随动**=① B4 `reward_economy` 行「消费依赖」列现写 `effect_engine(事件发射 dispatch——上层消费基础层)`,缝拆后须改「经注入 Callable 向 BattleFxHost 取偏运合计」;② `BattleFxHost` 行职责/暴露面补该查询口;③「战斗编排域拆件定稿」跨件单向接线款补一枚注入 Callable | 反哺点 · 列清单(②:三处逐条列明;实现批禁写 design/,落文归文档批) |
|
||||
|
||||
## 7. 任务卡
|
||||
|
||||
@@ -105,23 +111,28 @@
|
||||
| `game/presentation/views/confirm_dialog.gd` | p2 注释随修 :40-41(复用既有·修改) | 非行为(注释) |
|
||||
| `game/presentation/row_pacer.gd` | 零改动(flush :49 现成被消费) | — |
|
||||
| `game/core/board_generator.gd` | p5 handler 注入面(复用既有·修改) | 非行为(缺省零变化)→ wr5 |
|
||||
| `game/core/reward_economy.gd` | p5 同上(复用既有·修改) | 非行为 → wr5 |
|
||||
| `game/core/battle_orchestrator.gd` | p5 组装接线两行(:285/:290 后各一行)(复用既有·修改) | 非行为(接线)→ wr5 |
|
||||
| `game/core/reward_economy.gd` | p5 `_fatal` handler 注入+v1.4 缝拆(删 `_fx`/`_ctx`/`set_fx`,增 `luck_bias_query`)(复用既有·修改) | 非行为(缺省零变化)→ wr5/wr10/wr11 |
|
||||
| `game/core/battle_fx_host.gd` | p5 缝拆落点:追加公开方法 `luck_bias_for(cell_count)`(件由迭代 40 卡① 建,本卡只在件内加口)(复用既有·修改) | 非行为(判定搬运)→ wr10/wr11 |
|
||||
| `game/core/battle_orchestrator.gd` | p5 组装接线:fatal 两行+`set_fx` 一行换 `luck_bias_query` 一行(1 换 1)(复用既有·修改) | 非行为(接线)→ wr5/wr10 |
|
||||
| `game/core/rng_service.gd` | p6 handler+return(未知标签分支)+p7 同款(未播种分支)(复用既有·修改) | 非行为(缺省零变化)→ wr6/wr7 |
|
||||
| `game/tests/unit/core/run_time_fatal_test.gd` | **新增负面套件**(wr5+wr6+wr7 三测试点;名系提案,终名属白名单 2 代拍板范围)(新增文件) | 断言面(退出码判据) |
|
||||
| `game/tests/run_all.gd` | manifest 登记一行 `expect=1, negative=true`(:73 先例同款) | 闸登记面 |
|
||||
| `game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd` | **零改动**(v1.2 曾把 wr5 写进本件,v1.3 迁出——现役非负面套件禁承载违约取证) | — |
|
||||
| `game/tests/unit/battle_settlement/variant_placement_test.gd` | p5 缝拆验收载体:追加三条 `Kit.check`(判定面/接线面/权重生效面,见 wr10)(复用既有·修改) | 断言面新增(零删改既有断言)→ wr10 |
|
||||
|
||||
(新增一处=运行期违约通道负面套件+manifest 登记一行;其余为既有件内成员/分支/行级改动,p5/p6/p7 是成员变量+分支段。)
|
||||
(新增一处文件=运行期违约通道负面套件+manifest 登记一行;v1.4 另在 `battle_fx_host.gd` 追加一枚公开方法、在 `variant_placement_test.gd` 追加三条断言——均属既有件内的成员/方法/行级改动,无新文件)
|
||||
|
||||
### 接口块
|
||||
|
||||
- **p5 形态**(两件同款):`var fatal_handler: Callable = Callable()`(成员声明,注释「违约中止通道——缺省退化 push_error+assert;组装层注入 orchestrator._fatal 经封印真中止」);`_fatal` 体内 assert 前 `if fatal_handler.is_valid(): fatal_handler.call(msg)`。
|
||||
- **p5 接线**:orchestrator 组装点 `:285`(`_board_generator = BoardGenerator.new()`)后 `_board_generator.fatal_handler = _fatal`、`:290`(`_reward_economy = RewardEconomy.new(db, rng)`)后 `_reward_economy.fatal_handler = _fatal`(行号=v1.3 按迭代 38 合回后 master 重锚;orchestrator 自身 `_fatal` 现址 :1304)。
|
||||
- **p5 形态(fatal 通道,两件同款)**:`var fatal_handler: Callable = Callable()`(成员声明,注释「违约中止通道——缺省退化 push_error+assert;组装层注入 orchestrator._fatal 经封印真中止」);`_fatal` 体内 assert 前 `if fatal_handler.is_valid(): fatal_handler.call(msg)`。
|
||||
- **p5 接线(fatal)**:`_board_generator = BoardGenerator.new()` 之后 `_board_generator.fatal_handler = _fatal`、`_reward_economy = RewardEconomy.new(db, rng)` 之后 `_reward_economy.fatal_handler = _fatal`;行号与现形按迭代 40 卡① 合回后 T0 重锚(`BattleOrchestrator` 自身 `_fatal` 的 battle_result 封印是唯一阻断面)。
|
||||
- **p5 缝拆·宿主侧形态**:`func luck_bias_for(cell_count: int) -> int`(落 `game/core/battle_fx_host.gd`)——体内按契约 7 三参形 dispatch,命中里取 `kind == "bias_rewards"` 的 `value` 求和返回,零命中返回 0。该件公开方法 11→12(阈值 20)、类变量不变。
|
||||
- **p5 缝拆·掷格侧形态**:`var luck_bias_query: Callable = Callable()`;头注按 code-conventions §2.1 成文(参数语义=本次 install 可掷格数、返回值=偏运权重加算量、消费方=`_biased_cell_rows` 入参、缺省空回调=恒 0);`roll_board` 内 :65-69 的五行合算改形为 `var luck_bias := 0` + `if luck_bias_query.is_valid(): luck_bias = luck_bias_query.call(list.size())`,调用位仍在开流之后、`_biased_cell_rows(luck_bias)` 之前。
|
||||
- **p5 缝拆·接线**:组装点 `_reward_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx)` 一行 → `_reward_economy.luck_bias_query = _fx_host.luck_bias_for`(1 换 1;禁在掷格件内重建引擎引用)。
|
||||
- **p6 形态**:`var violation_handler: Callable = Callable()`;未知标签分支=push_error + `if violation_handler.is_valid(): violation_handler.call(purpose)` + `assert(false, …)`(handler 有效时调用在 assert 前——debug 注入测试面可断言)+ `return null`。
|
||||
- **p7 形态**(v1.3 新增):`_master_seed < 0` 分支同款——push_error + 复用 p6 的 `violation_handler`(有效则调用,传 purpose 标签)+ `assert(false, …)` + `return null`;缺省(不注入)时行为逐字等于现状(契约 2 同样约束 p7)。分支次序事实:未知标签检查(:101-103)在播种检查(:104-106)之前,故 p7 通道只能用**在册合法标签**触达。
|
||||
- **wr5/wr6/wr7 三点**:全部落**一个新增负面套件** `game/tests/unit/core/run_time_fatal_test.gd`(extends SceneTree,退出码判据:全绿尾行 `quit(1)`、回归态 `quit(2)`——`extended_row_violation_test` 同款形态),manifest 登记 `{"path": …, "expect": 1, "negative": true}`。
|
||||
- **载体三纪律**(v1.3 复核轮钉死,违此即 L3 停呈):① **注入只经编排器组装点**(:285/:290 两行接线),**禁在测试体内手工 `bg.fatal_handler = oc._fatal`**——手工接线会让撤销证明②(回退组装点)空转;② 三测试点**各用新建 `RngServiceCore` 实例**,禁跨点复用、禁跑过取号后再播种(`set_master_seed` 自带 `assert(_counter == 0)`,:60——复用实例必炸在播种步);③ **回归态判据只能取「改前改后真有差异」的可观测面**——可差异的面只有两处:handler **成员与调用点是否存在**(记录位真假)与**封印是否落**。`return` 位与计数器读数在 debug 下不可判别:assert 已先中断 `open_stream` 本体,「装了 return」与「删掉 return」两态同样到不了 `:107-108` 的推计数段、同样表现为「拿不到可用 RNG」——**拿这两面当撤销证明=必假绿**;故 p6/p7 的 `return` 位按 wr3 同款「静态判据+代码评审注记(+grep 该分支 return 行在位)」承接,**禁写「违约行照发号/未播种照取号」式运行期判据**(照实装必触发 §9 L3 再停卡)。
|
||||
- **载体三纪律**(v1.3 复核轮钉死,违此即 L3 停呈):① **注入只经编排器组装点**(`_board_generator = BoardGenerator.new()`/`_reward_economy = RewardEconomy.new(db, rng)` 之后的 fatal 接线两行,行号按迭代 40 卡① 落定形态 T0 重锚),**禁在测试体内手工 `bg.fatal_handler = oc._fatal`**——手工接线会让撤销证明②(回退组装点)空转;② 三测试点**各用新建 `RngServiceCore` 实例**,禁跨点复用、禁跑过取号后再播种(`set_master_seed` 自带 `assert(_counter == 0)`,:60——复用实例必炸在播种步);③ **回归态判据只能取「改前改后真有差异」的可观测面**——可差异的面只有两处:handler **成员与调用点是否存在**(记录位真假)与**封印是否落**。`return` 位与计数器读数在 debug 下不可判别:assert 已先中断 `open_stream` 本体,「装了 return」与「删掉 return」两态同样到不了 `:107-108` 的推计数段、同样表现为「拿不到可用 RNG」——**拿这两面当撤销证明=必假绿**;故 p6/p7 的 `return` 位按 wr3 同款「静态判据+代码评审注记(+grep 该分支 return 行在位)」承接,**禁写「违约行照发号/未播种照取号」式运行期判据**(照实装必触发 §9 L3 再停卡)。
|
||||
- ① wr5 封印传导(p5):装配构造真实编排器(V6 形态,`round_flow_skill_pool_test` 现成装配链可照抄)→ 违约经**组装点注入的** handler 传导 → 直调 `_board_generator._fatal("测试违约")` → 验 `oc.battle_result == "fatal_error"`(`_fatal` :1304 先落封印再 assert);**回归态**=组装点接线被移除 → 协作件违约不落封印 → `quit(2)`。
|
||||
- ② wr6 未知标签违约通道(p6):新建实例 → 注入记录型 `violation_handler` → `open_stream("bogus_purpose")`(出词表)→ 验 handler **记录位为真**(判据主体)+拿不到可用 RNG;**对照**=新实例播种后取在册合法标签(`board_gen`)返回可用 RNG 且 `counter()` +1;**回归态判据**=handler 成员与调用点不存在(未实装 p6 形态)时注入无处可注 → 记录位守不住 → `quit(2)`;`return` 位本体不可观测,走 grep 在位+评审注记。
|
||||
- ③ wr7 未播种取数通道(p7):新建实例**不播种**(`_master_seed` 缺省 -1,:23)→ 注入 handler → `open_stream("board_gen")`——**必须用在册合法标签**:`PURPOSE_TAGS`(:9)不含 `adventure`,且未知标签检查排在播种检查之前,出错标签会让本点退化成测试点②(撤销证明④空转);验 handler **记录位为真**+拿不到可用 RNG;**对照**=同标签播种后照常取号;**回归态判据**=该分支 handler 调用点缺失 → 记录位守不住 → `quit(2)`;`return` 位同走静态面。
|
||||
@@ -132,30 +143,31 @@
|
||||
### 落位与可复用性预检
|
||||
|
||||
- **新增件一处**:`game/tests/unit/core/run_time_fatal_test.gd`(运行期违约通道负面套件)——**为什么必须新增**=违约取证必产不可捕获的 SCRIPT ERROR 行,现役非负面套件被闸 02 扫描覆盖,落位无处可复用(req-card 第 13 条落位预检的答题;闸侧 `negative=true` 豁免机制与 `extended_row_violation_test` 形态照抄,闸件本身零改动)。其余全部为既有件内成员/分支/行级改动;flush/pending_rows 复用 row_pacer 现成公开面。
|
||||
- **复用面**:p5 复用 orchestrator 既有 `_fatal` 封印(不造第二中止机制——审计「可经宿主注入」的最小实现);p4 复用 flush();p2 复用 ModalFlow 单口既有编排;p7 复用 p6 的 `violation_handler` 成员(不新开第二回调面);负面套件复用 run_all 的 `negative` 豁免通道。
|
||||
- **唯一面**:违约通道各件唯一(`fatal_handler` 两件各一处;`violation_handler` rng_service 单件一处、覆盖未知标签与未播种两条分支;组装接线单点);confirm 开合收口后单口唯一。
|
||||
- **分层**:core 件零 UI 依赖不破(Callable 注入非 UI 引用;组装接线在 core 内层间);presentation 收口经既有 ModalFlow。
|
||||
- **复用面**:p5 复用 orchestrator 既有 `_fatal` 封印(不造第二中止机制——审计「可经宿主注入」的最小实现)+复用 `BattleFxHost` 既有的 `engine`/`counters_ctx` 持有(判定搬进已持该引擎的件,不新建宿主、不新开第二处引擎引用);p4 复用 flush();p2 复用 ModalFlow 单口既有编排;p7 复用 p6 的 `violation_handler` 成员(不新开第二回调面);负面套件复用 run_all 的 `negative` 豁免通道。
|
||||
- **唯一面**:`bias_rewards` 判定全仓单点(从掷格件搬进宿主件一枚公开方法,掷格件只留 `luck_bias_query` 一处回调);违约通道各件唯一(`fatal_handler` 两件各一处;`violation_handler` rng_service 单件一处、覆盖未知标签与未播种两条分支;组装接线单点);confirm 开合收口后单口唯一。
|
||||
- **分层**:core 件零 UI 依赖不破(Callable 注入非 UI 引用;组装接线在 core 内层间);v1.4 注入方向单点——`RewardEconomy` 指 `BattleFxHost` 的公开方法,宿主不持掷格件引用=禁 RefCounted 环(同迭代 40 卡① 契约 5);presentation 收口经既有 ModalFlow。
|
||||
|
||||
### T0 事实坐实(≈0.1d)
|
||||
|
||||
- [ ] 复核 S1–S7(重点:S2 ModalFlow.open 与 confirm_dialog.open 先后序、S6/S7 两分支现行行号、wr2 探针段定位);确认 pacing 回归面与 `round_flow_skill_pool_test` 装配链(wr5 ① 照抄用)。可核查结果:定位清单进实现报告素材。
|
||||
- [ ] 复核 S1–S8(重点:S2 ModalFlow.open 与 confirm_dialog.open 先后序、S6/S7 两分支现行行号、S8 的 `RewardEconomy` 类变量现值与 `_fx`/`_ctx` 消费点);p5 面按迭代 40 卡① 落定形态重锚(`battle_fx_host.gd` 是否已建、`BattleFxHost` 公开方法计数、组装点现形与行号);确认 pacing 回归面与 `round_flow_skill_pool_test` 装配链(wr5 ① 照抄用);采改前 dump 基线=`sim/dump_battle.gd` 六组(种子 {42, 779401, 779501}×关卡 {1, 2})JSONL 留盘(wr11 对照)。可核查结果:定位清单+改前六组基线路径进实现报告素材。
|
||||
|
||||
### T1 定点修六件+负面套件(≈0.5d)
|
||||
|
||||
- [ ] 实装 p1/p3/p4(删句/防护/flush)+wr4 带夹具断言。可核查结果:grep 死变量与重复组零命中(:123-124 保留段除外);pacing_queue_test 直跑绿;wr4 断言绿。
|
||||
- [ ] 实装 p5/p6/p7(handler 面+接线+两分支 return)。可核查结果:缺省形态断言绿(契约 2);负面套件三测试点尚未建时先直跑 rng/board_generator 相关现役套件确认合法路径零红。
|
||||
- [ ] 实装 p5 缝拆三处(掷格件成员替换+宿主查询口 `luck_bias_for`+组装点一行换一行,体内按契约 7 三参形)。可核查结果:`RewardEconomy` 类变量实测 15(`python tools/gdstyle_gate.py` 直扫零 NEW 键、`tools/gdstyle_baseline.txt` 零改动);`bias_rewards` 判定单点在 `battle_fx_host.gd` 在位;缺省态(不注入)`roll_board` 输出与改前逐字同。
|
||||
- [ ] 实装 p5 fatal 通道+p6/p7(两件 `fatal_handler` 面+组装接线两行+rng 两分支 handler 与 `return null`)。可核查结果:缺省形态断言绿(契约 2);负面套件三测试点尚未建时先直跑 rng/board_generator 相关现役套件确认合法路径零红。
|
||||
- [ ] 建负面套件 `run_time_fatal_test.gd` 三测试点+`run_all.gd` manifest 登记一行(`expect=1, negative=true`)+**新件的 `.gd.uid` 边车随批入库**(`.gitignore:6`「.uid 必须提交」——迭代 38 卡④ 曾漏带边车留未跟踪态)。可核查结果:本件直跑退出码恰 1;闸 02 对本件不判红(负面豁免生效);登记行与 `:73` 先例形态逐字段一致;`git status` 无残留 `?? *.uid`。
|
||||
- [ ] 实装 p2(confirm 三处收口+注释)。可核查结果:wr2 回归绿;grep `confirm_dialog.open(`/`.close()` 直调仅剩方法定义与收口内调用。
|
||||
- [ ] 验证步骤:全套闸按域选档跑一次全绿。
|
||||
|
||||
### T2 撤销证明+产出物落位卡(≈0.15d,末卡)
|
||||
|
||||
- [ ] 撤销证明四条:①回退 p1 hunk(死变量还原)→ grep `battle_live` 命中即红;②回退 p5 组装点接线两行 → 协作件违约不落封印 → 本件退出码 0 ≠ expect 1(wr5 测试点① `quit(2)`)→ 还原复绿;③回退 p6 hunk(handler 成员与调用点整体不存在→注入无处落)→ wr6 记录位守不住 → 退出码非 1 → 还原复绿;④回退 p7 hunk(同上,针对未播种分支调用点)→ wr7 记录位守不住 → 退出码非 1 → 还原复绿。**三条负面套件套一律以退出码为判据,禁写「断言变红」式口径**(assert 不改进程退出码,红只落在 `quit(2)`);p6/p7 的 `return` 位不作运行期判据(debug 下两态不可判别,见契约 6),以「grep 该分支 return 行在位+代码评审注记」出证并在实现报告如实标注观测面局限。记录进实现报告。
|
||||
- [ ] 清单同步:`python tools/extract_checks.py --extract --force` 重生成 `game/tests/验证集清单.md`(清单已存在时无 `--force` 拒写并返 2),再 `--diff` 对账零差异。**预期零新增行**:提取器只认 `check(` 形态,负面套件以 `quit()` 承载、`extended_row_violation_test` 在册先例即零 check 行;新件仍会进清单头部的源文件列表——该变化不算断言新增,禁为凑行去补 `Kit.check`。
|
||||
- [ ] 撤销证明五条:①回退 p1 hunk(死变量还原)→ grep `battle_live` 命中即红;②回退 p5 组装点 fatal 接线两行 → 协作件违约不落封印 → 负面套件退出码 0 ≠ expect 1(wr5 测试点① `quit(2)`)→ 还原复绿;③回退 p6 hunk(handler 成员与调用点整体不存在→注入无处落)→ wr6 记录位守不住 → 退出码非 1 → 还原复绿;④回退 p7 hunk(同上,针对未播种分支调用点)→ wr7 记录位守不住 → 退出码非 1 → 还原复绿;⑤删 p5 缝拆的组装注入一行(`luck_bias_query` 停为缺省 `Callable()`)→ wr10 ②③ 断言红(① 测宿主面不红,恰证判据钉在接线上)→ 还原复绿。**三条负面套件套一律以退出码为判据,禁写「断言变红」式口径**(assert 不改进程退出码,红只落在 `quit(2)`);p6/p7 的 `return` 位不作运行期判据(debug 下两态不可判别,见契约 6),以「grep 该分支 return 行在位+代码评审注记」出证并在实现报告如实标注观测面局限。记录进实现报告。
|
||||
- [ ] 清单同步:`python tools/extract_checks.py --extract --force` 重生成 `game/tests/验证集清单.md`(清单已存在时无 `--force` 拒写并返 2),再 `--diff` 对账零差异。**预期新增三条**(wr10 判定面/接线面/权重生效面,落 `variant_placement_test.gd`,均为 `Kit.check` 形态);负面套件仍以 `quit()` 承载、零 check 行(`extended_row_violation_test` 在册先例),新件进清单头部源文件列表不算断言新增,禁为凑行补 `Kit.check`。
|
||||
- [ ] 写齐三份产出物(实现报告〔六件逐件核对表+wr 断言读数+实机目检总清单——选档删除确认流目检一项(p2 锁域归位后行为目检)〕/代拍板追认单/反哺点清单〔§6 逐条落点〕)。
|
||||
- [ ] 合并前置:独立验收审计(§8.3)逐行「通过」。
|
||||
|
||||
**估时合计 ≈0.85d**(v1.3 停卡修订随增 ≈0.1d=负面套件新件+manifest 登记+清单对账;p7 并修增量落在同一 handler 形态内,随增 0)。编排注记:六件皆小而独立——与卡 A 同迭代分波无冲突;p2 有锁域行为面(wr2 回归是合并前置);**v1.3 起验收载体已定死为负面套件,编排段与实现段禁再把 wr5/wr6/wr7 挪回现役套件**。
|
||||
**估时合计 ≈1.0d**(v1.3 ≈0.85d + v1.4 缝拆增量 ≈0.15d=宿主查询口一枚、掷格件成员替换、wr10 三条断言、dump 六组改前基线采集与批末 diff;p7 并修增量落在同一 handler 形态内,随增 0)。编排注记:六件皆小而独立——与卡① 同迭代按卡序施工(p5 硬前置见 §0);p2 有锁域行为面(wr2 回归是合并前置);**v1.3 起验收载体已定死为负面套件、v1.4 起 p5 已定死为缝拆三处,编排段与实现段禁再把 wr5/wr6/wr7 挪回现役套件、禁改回「只加 fatal_handler」的老处方**。
|
||||
|
||||
## 8. 验证闸
|
||||
|
||||
@@ -167,37 +179,39 @@
|
||||
| wr2 confirm 收口锁域归位(p2) | main_overlay_flow.gd:88+main.gd:311/320 | save_select/confirm 回归探针段〔wr2〕+锁域断言 | 探针 | 选档删除确认真实流(探针驱动) | 回退 p2 hunk → 收口判据 grep 命中即红+锁域断言红 |
|
||||
| wr3 入口 null 防护(p3) | main.gd:721/748 前置防护 | 防护分支单测/静态判据〔wr3〕(防护态构造经探针锁谓词异常态不可达——静态面+代码评审注记申报) | 静态 | — | 回退 p3 hunk → grep 防护行零命中红 |
|
||||
| wr4 会话 flush(p4) | main.gd:175 一行 | pacing_queue_test+会话更替断言〔wr4〕(新增:**前置夹具入队若干行再调 begin_session**,断言其后 pending_rows()==0——无夹具则改前改后皆绿钉不住〔grilling 订正〕) | 单元 | _begin_session 真实调用路径+enqueue 真实入队面 | 回退 p4 hunk → 带夹具断言红(残留非零) |
|
||||
| wr5 fatal 封印传导(p5) | 两件 handler+orchestrator 组装点接线(:285/:290) | 负面套件测试点①(含缺省形态断言;注入只经组装点,禁测试内手工接线)〔wr5〕 | 单元(负面) | V6 装配构造真实编排器路径 | 回退接线两行 → 本件退出码 0 ≠ expect 1(T2②) |
|
||||
| wr5 fatal 封印传导(p5) | 两件 handler+orchestrator 组装点 fatal 接线两行(行号按卡① 形态 T0 重锚) | 负面套件测试点①(含缺省形态断言;注入只经组装点,禁测试内手工接线)〔wr5〕 | 单元(负面) | V6 装配构造真实编排器路径 | 回退接线两行 → 本件退出码 0 ≠ expect 1(T2②) |
|
||||
| wr6 未知标签违约通道(p6) | rng_service 未知标签分支 handler+return | 负面套件测试点②(handler 记录位为判据主体+拿不到可用 RNG+在册标签对照)〔wr6〕 | 单元(负面) | open_stream 真实调用路径 | 回退 p6 hunk(成员与调用点整体消失→注入无处落)→ 记录位守不住 → 退出码非 1(T2③);`return` 位=grep 在位+评审注记(debug 下两态不可判别,禁当运行期判据) |
|
||||
| wr7 未播种取数通道(p7) | rng_service 未播种分支 handler+return | 负面套件测试点③(在册合法标签+不播种实例;handler 记录位+拿不到可用 RNG+播种对照)〔wr7〕 | 单元(负面) | 未播种的 open_stream 真实路径 | 回退 p7 hunk(该分支调用点缺失)→ 记录位守不住 → 退出码非 1(T2④);`return` 位同走静态面 |
|
||||
| wr8 负面载体登记 | 新件+`run_all.gd` manifest 一行 | 登记行在位判据〔wr8〕(`expect=1` 与 `negative=true` 两字段齐备,静态 grep+评审注记——全仓无闸对账盘上测试文件与 manifest,禁写「删登记行即红」的不存在判据)+闸 02 对本件不判红+验证集清单 --diff 零差异 | 闸登记 | run_all 真实批跑路径 | 删 `negative=true` 标记而保留登记行 → 本件输出被闸 02 捕获判 FAIL |
|
||||
| wr9 产出物 | T2 三份 | 件核〔wr9〕 | — | — | 缺项红 |
|
||||
| wr10 偏运判定收口+注入接线(p5 缝拆) | `battle_fx_host` 新增 `luck_bias_for`+`reward_economy` 成员替换+组装点一行换一行 | `variant_placement_test.gd` 三条断言:①判定面=夹具经宿主 engine 装载一枚 `reward_roll`→`bias_rewards value=2` 订阅(现网无此类数据行,白盒装载先例=`tree_rules_v11_test` 读 `_triggers`),断言 `oc.fx_host().luck_bias_for(4) == 2` 且空订阅 == 0;②接线面=真装配编排器断言 `oc._reward_economy.luck_bias_query.is_valid()` 且与 `oc.fx_host().luck_bias_for` 同源;③权重生效面=`eco._pool_weight(eco._biased_cell_rows(eco.luck_bias_query.call(4))) == 77 + 4 * 2`(与现役 :238-241 白盒合成序断言同值)〔wr10〕 | 单元 | 编排器 `_init` 组装链+`begin_battle` 真链 | 删组装注入一行 → `luck_bias_query` 无效 → bias 恒 0 → ②③ 红(① 不红恰证判据钉在接线上)→ 还原复绿(T2⑤) |
|
||||
| wr11 合法路径逐位(p5 缝拆总断言,兜契约 1/契约 7) | p5 缝拆三处的改动总和(总断言行,无独立改动) | `sim/dump_battle.gd` 六组(种子 {42, 779401, 779501}×关卡 {1, 2})批末复跑与 T0 改前 JSONL 逐行 diff——非空即红,零新码(迭代 40 卡① wr10 同款机制)〔wr11〕 | 集成 | `--script res://sim/dump_battle.gd` 直跑(关卡×种子矩阵) | 回退任一缝拆 hunk → 复跑出差异(发射面或取号序变)→ 还原复绿 |
|
||||
|
||||
### 8.2 测试削弱申报
|
||||
|
||||
零(既有断言零改动;新增断言四条=wr4 夹具断言+wr2 锁域断言(若现役未覆盖)+负面套件内两处缺省形态断言,其中违约三点以退出码承载、非 `Kit.check` 形态;wr5/wr6/wr7 由现役套件迁至独立文件属**载体迁移**,被迁走的断言在 v1.2 从未合回 master,不存在削弱既有验证面;wr3 防护分支不可达性以静态判据+申报承载——不虚构不可达态的运行期断言)。
|
||||
零(既有断言零改动;新增断言七条=wr4 夹具断言+wr2 锁域断言(若现役未覆盖)+wr10 三条(判定面/接线面/权重生效面,`Kit.check` 形态进清单)+负面套件内两处缺省形态断言,其中违约三点以退出码承载、非 `Kit.check` 形态;wr5/wr6/wr7 由现役套件迁至独立文件属**载体迁移**,被迁走的断言在 v1.2 从未合回 master,不存在削弱既有验证面;wr3 防护分支不可达性以静态判据+申报承载——不虚构不可达态的运行期断言;wr11 复用既有 `sim/dump_battle.gd`,零新码零删改)。
|
||||
|
||||
### 8.3 独立验收审计
|
||||
|
||||
入口=未参与实现的会话或子代理;报告落 `reports/iter<N>-验收审计-<日期>.md`(多卡同批须带卡号位防撞名);抽查重点=契约 1/2(合法路径逐位不变——缺省形态断言在案)、p2 锁域回归读数、p5 接线单点、p6/p7 两分支 return 位置(assert 语义保持)、**契约 6 与载体三纪律四判据**=① manifest 的 `negative=true`+`expect=1` 在册;② 三测试点回归态判据真能变红且**只取有差异面**(变异注入实测:抹掉 handler 成员与调用点/拆掉组装接线 → 退出码非 1;若审计发现判据落在 `return` 位或计数器这类 debug 下两态不可辨的面=阻断,防假绿);③ 注入面只在编排器组装点(`grep fatal_handler` 在测试文件内零命中——防撤销证明②空转);④ 三测试点各用新建实例(复用实例会撞 `assert(_counter == 0)`,撞即证纪律失效)。未过不得合并。
|
||||
入口=未参与实现的会话或子代理;报告落 `reports/iter<N>-验收审计-<日期>.md`(多卡同批须带卡号位防撞名);抽查重点=契约 1/2(合法路径逐位不变——缺省形态断言在案)、p2 锁域回归读数、p5 接线单点、p6/p7 两分支 return 位置(assert 语义保持)、**契约 6 与载体三纪律四判据**(见 v1.3 段)、**v1.4 缝拆三判据**=①`luck_bias_for` 体内 dispatch 为三参形(`grep` 宿主件内该方法体零 `fx_emit` 复用、零第四实参);②`RewardEconomy` 类变量实测 15(`python tools/gdstyle_gate.py` 现算与 `tools/gdstyle_baseline.txt` 逐字节零变化,禁以 --refresh 换绿);③wr10 三条断言非空转(夹具装载订阅命中数 >0 前置写在断言消息里,命中为 0 即 L3;防「扫描面为空照样绿」)。未过不得合并。
|
||||
|
||||
## 9. 异常分级
|
||||
|
||||
- **L1(自救)**:p2 收口先后序形态;p5/p6/p7 注释措辞;断言挂点与负面套件事件命名(在 `*_violation_test`/`*_fatal_test` 既有约定内)。
|
||||
- **L2(记录继续)**:行号漂移;manifest 登记行的注释尾缀现位变化。
|
||||
- **L3(停机呈报)**:ModalFlow.open 与 confirm_dialog.open 语义冲突(双显示/挂起兜底破坏确认流——收口形态失配停呈);p4 flush 引入排播节奏变化(契约 4 破);p5 接线后协作件违约不落封印(传导断链);发现第三处 `_fatal` 同型软回退(审计清单外);负面套件三测试点的回归态判据不变红(抹掉 handler 成员与调用点、或拆掉组装接线后本件仍出 1——说明判据取到了 debug 下两态不可辨的面,停呈禁改落位自救);出现测试体内手工接线(违载体纪律①,撤销证明②会空转);跨测试点复用同一 rng 实例致播种撞 `assert(_counter == 0)`(违纪律②——改正方向是每点新建实例,禁动播种断言)。
|
||||
- **L3(停机呈报)**:ModalFlow.open 与 confirm_dialog.open 语义冲突(双显示/挂起兜底破坏确认流——收口形态失配停呈);p4 flush 引入排播节奏变化(契约 4 破);p5 接线后协作件违约不落封印(传导断链);发现第三处 `_fatal` 同型软回退(审计清单外);**v1.4 新增三条**=p5 缝拆后 dump 六组出差异(=发射面或取号序被动,契约 1/7 破);`RewardEconomy` 类变量实测出第 16 枚、或 fatal 接线使 `battle_orchestrator` 的 `max-file-length` 较登记 1302 恶化(禁抬基线,停呈由用户裁拆法);wr10 ① 夹具装载的订阅命中为 0(判据空转,停呈禁改判据自救);负面套件三测试点的回归态判据不变红(抹掉 handler 成员与调用点、或拆掉组装接线后本件仍出 1——说明判据取到了 debug 下两态不可辨的面,停呈禁改落位自救);出现测试体内手工接线(违载体纪律①,撤销证明②会空转);跨测试点复用同一 rng 实例致播种撞 `assert(_counter == 0)`(违纪律②——改正方向是每点新建实例,禁动播种断言)。
|
||||
- v1.2 曾列的「p6 同函数 master_seed<0 分支(:104-106)单通道停卡呈报」条款**已由用户裁并修,随 v1.3 出账**(转 §1 p7+wr7);同型第四处仍走本条停呈通道。
|
||||
|
||||
## 10. 代拍板白名单(超清单必呈报)
|
||||
|
||||
1. p5/p6/p7 成员命名与注释措辞(p7 复用 p6 的 `violation_handler`,禁另开第二成员)。
|
||||
1. p5/p6/p7 成员命名与注释措辞(`fatal_handler`/`violation_handler` 沿用 v1.3;p7 复用 p6 的 `violation_handler`,禁另开第二成员);**v1.4 的 `luck_bias_query`/`luck_bias_for` 两名已由用户 2026-10-27 钉死,禁改名**,注释措辞可自定;wr10 ① 判定面夹具的装载形态(`_push_trigger` 白盒装载或等价现成口)在 `tree_rules_v11_test` 先例内自选。
|
||||
2. wr 断言挂点与消息文本、负面套件文件名终名(提案名 `run_time_fatal_test.gd`,须落 `*_violation_test`/`*_fatal_test` 既有约定内)。
|
||||
3. p2 收口的先后序实现形态(ModalFlow.open 语义 T0 核后定形)。
|
||||
4. 提交切分与 worktree 分支命名。
|
||||
|
||||
## 11. 待裁点
|
||||
|
||||
**零待裁点,开工门=U0。** 灰色推导四处(呈报可否决,随 T2 反哺点):①p5 走「组装层传导 orchestrator 封印」而非回调穿透到 presentation——依据=core 件零 UI 依赖红线+复用既有封印零新中止机制;②wr3 防护分支验收用静态判据不造运行期断言——依据=分支在锁谓词正常态不可达,虚构异常态夹具违背「真实入口」口径;③wr5/wr6/wr7 载体取独立负面套件而非改造闸 02 扫描规则——依据=闸豁免机制本为此设且已在册三件先例,改扫描规则属动闸件(工程改动面)且会削弱「断言不终止进程」的防假绿能力(test-conventions §5 三硬闸的存在理由);④p7 并修而非另立——依据=用户 2026-10-26 裁(实现报告 §三建议案),且 p7 与 p6 同函数同形态、复用同一 handler 成员,改动面仍封闭在 §1 所列行内(违不违契约 1 的答案=不违,因契约 1 的名单随 v1.3 扩到 p7)。
|
||||
**零待裁点,开工门=U0。** 灰色推导六处(呈报可否决,随 T2 反哺点):①p5 走「组装层传导 orchestrator 封印」而非回调穿透到 presentation——依据=core 件零 UI 依赖红线+复用既有封印零新中止机制;②wr3 防护分支验收用静态判据不造运行期断言——依据=分支在锁谓词正常态不可达,虚构异常态夹具违背「真实入口」口径;③wr5/wr6/wr7 载体取独立负面套件而非改造闸 02 扫描规则——依据=闸豁免机制本为此设且已在册三件先例,改扫描规则属动闸件(工程改动面)且会削弱「断言不终止进程」的防假绿能力(test-conventions §5 三硬闸的存在理由);④p7 并修而非另立——依据=用户 2026-10-26 裁(实现报告 §三建议案),且 p7 与 p6 同函数同形态、复用同一 handler 成员,改动面仍封闭在 §1 所列行内(违不违契约 1 的答案=不违,因契约 1 的名单随 v1.3 扩到 p7);⑤**v1.4** `luck_bias_for` 不带第四实参 facts——依据=逐位不变优先于「顺手把事实面接通」,接通 facts 会打开订阅行 `when` 条件面,其归位属端口设计题(归新登记条目);⑥**v1.4** wr10 ① 判定面用白盒装载订阅——依据=现网 `game/data/tables/` 无任何产出 `bias_rewards` 的行(S8 实查),走真实内容行无处可取,白盒装载有 `tree_rules_v11_test` 在册先例;代价随 T2 反哺点呈报(`bias_rewards` 词汇进宿主层一行)。
|
||||
|
||||
## 12. 自审六查与复核记录
|
||||
|
||||
@@ -210,3 +224,5 @@
|
||||
**迭代 38 实现段停卡呈报(2026-10-26,非复核件·上游缺陷移交)**:p1–p6 实装完成后,wr5/wr6 落位核查发现 v1.2 处方不成立——headless 下 `assert` 失败必产 `SCRIPT ERROR`、不可捕获且只中断当前函数,而 `run_all.gd` 对非负面套件的判定是「输出含该标记即 FAIL」(闸 02,test-conventions §5 三硬闸之二),故「wr5/wr6 绿+全套闸全绿」在同一跑次内互斥。改动全数还原、零提交(WIP 补丁 172 行留 `C:/Users/DK/AppData/Local/Temp/iter38_card2_wip.patch`;重启按合回后 master 重核行号重做,禁直接 apply)。同款缺陷卡④ 在 grilling 一轮已撞见并以独立负面套件承载(`extended_row_violation_test`,manifest `expect=1, negative=true`)——卡② 两轮复核未复现该判点,因直跑单套件不经过闸 02 扫描,只有批末全量闸才暴露。
|
||||
|
||||
**v1.3 裁定与判据(2026-10-26,用户裁「把需求变更给修正确」)**:①违约取证(wr5/wr6/wr7)只落**一个新增负面套件**+manifest 显式登记 `expect=1, negative=true`,现役非负面套件零改动;②`master_seed < 0` 分支经用户裁并修,立为 p7+wr7;③p7 触达前提是播种检查排在未知标签检查之后,故测试点③必须在册合法标签;④回归态判据只取 debug 可观测面,release 独有位走静态判据+评审注记;⑤载体三纪律(组装点注入/每点新建实例/禁手工接线)与撤销证明四条(①grep 面,②③④退出码面)为完成定义;⑥估时 ≈0.85d;⑦闸 09 现为迭代 38 特批存量红,本批开工前须由用户裁「还债或刷新基线」,禁以「闸 09 绿」为完成条件;⑧件②独立卡已随迭代 38 卡① 合回退役,来源指址改 git 提交号;⑨条目归属随「gdstyle 存量债全量清偿」波 7。
|
||||
|
||||
**v1.4 用户裁定与现行口径(2026-10-27)**:迭代 40 卡② 施工至 p5 时实测 `RewardEconomy` 类变量 15=gdstyle 阈值顶(`tools/gdstyle_baseline.txt` 无该键,第 16 枚即闸 09 新增 FAIL),v1.3 处方「加 fatal_handler 一枚」在该件不可实现;基线只降不升,禁登记 16 抵账。现行口径=①p5 定三处(成员替换、`bias_rewards` 判定收回 `BattleFxHost` 公开方法、组装点一行换一行),`RewardEconomy` 类变量净 15;②p5 硬前置=迭代 40 卡①(落点件由该卡建);③闸 09 义务=零新增,登记值只降不升(v1.3 完成定义里的「还债或经批准刷新基线」二选一与此口径冲突,已废);④验收加 wr10(三条 `Kit.check`)与 wr11(`sim/dump_battle.gd` 六组逐位);⑤端口化与宿主瘦身、掷格/受理分件三条不属本卡,落待办清单「战斗域结构深拆批」条目(状态=待文档设计);⑥已知代价随 T2 反哺点呈报=`bias_rewards` 词汇进宿主层一行,端口化后归位。
|
||||
@@ -20,6 +20,20 @@
|
||||
|
||||
**状态**:文档设计已闭合+已写卡+已编迭代 40+已编迭代 41
|
||||
|
||||
#### 战斗域结构深拆批——效果发射端口+宿主瘦身+掷格/受理分件(2026-10-27 登记,来源=批次移交)
|
||||
|
||||
**实况**:三条同病根的结构债,需先一次设计讨论定端口形态,再谈切卡;`RewardEconomy` 的最小缝拆样本已由迭代 40 卡② 落卡。
|
||||
- 效果发射端口(设计题本体):`game/core/damage_controller.gd:46/47/71` 与 `game/core/action_economy.gd:44/45/50` 仍直持 `EffectEngine`+`EffectContext`——前者用 attack_commit/armor_roll/clamp_check/kill/damage_taken 五枚事件并带计数器读写回写(:348/:354),后者用 dig_paid/dig_stop/charge_full 三枚事件。`RewardEconomy` 同款直连只服务一枚 `bias_rewards` 求和,已由 reports/需求卡片-结构小件定点修批-2026-10-26.md v1.4 的 p5 缝拆成单枚查询口(成员 −2+2、类变量净 15)——最小样本成立;另两处事件多且带计数器回写,缝拆不成立,必须先设计「业务类只持一个回调、取回命中批」的端口形态。
|
||||
- battle_orchestrator 瘦身:迭代 40 卡① 拆出 `BattleFxHost`/`RewardGrant` 后宿主仍留 38 类变量(阈值 15;卡① §11 裁决记录④ 按「改善非清零」为该类变量键立了在册豁免——豁免不等于了结),`tools/gdstyle_baseline.txt` 在册 `game/core/battle_orchestrator.gd|quality/max-class-variables = 50` 与 `|quality/max-file-length = 1302` 两键,实际计数与行数以卡① 合回后实测重锚。
|
||||
- reward_economy 分件:`RewardEconomy` 现 15 类变量恰在阈值线上(加第 16 枚即出闸 09 新增红——迭代 40 卡② 施工实测撞见,正是本条登记起因),掷格面(install 期 roll/池行装配/权重合成)与受理账面(grant/身份查询/快照/retire)两职责同居一件。
|
||||
- 相关文档:design/engineering/architecture/architecture.md(系统分层图与功能层 B1/B4 行、「战斗编排域拆件定稿」节须随端口定稿改写)· reports/审计-gdstyle存量债清点与清偿方案-2026-10-26.md(216 键波次方案与残留债口径;在册「尾波」只覆盖迭代 39 三宿主,不含本条)· reports/需求卡片-结构小件定点修批-2026-10-26.md v1.4(缝拆样本与 2026-10-27 用户裁记载于该卡 §12)
|
||||
|
||||
**缺口**:效果发射端口形态未定稿(一枚回调的束形与返回值、计数器读写回写与 facts 取值的归属面、`bias_rewards` 等域词汇是否留在宿主层、是否与 `BattleFxHost` 同件承载);宿主保留面与掷格件的职责边界(哪些成员随件迁出、件怎么切)未裁定——需一次设计讨论并落 architecture.md 后才能写卡。
|
||||
|
||||
**改动面**:游戏代码
|
||||
|
||||
**状态**:待文档设计
|
||||
|
||||
#### 波 0:gdstyle 基线刷新——清 18 陈旧键+登记迭代 40 还债水位(2026-10-27 登记,来源=会话提及)
|
||||
|
||||
**实况**:证据=`tools/gdstyle_baseline.txt` 存量登记 234 键中有 18 条陈旧键不命中当前盘面(迭代 37 重构已消、键未清)+迭代 38 特批带红合并的恶化债(git 提交 c4c69701)待迭代 40 还债归绿——刷新后账本对齐现实。人工 `python tools/gdstyle_gate.py --refresh` 一次,清陈旧键 18 条(骤降 18/234≈7.7% 低于 10% 守卫线无需 --force)+登记迭代 40 还债后的新水位(卡① battle_orchestrator 行数归绿、卡① settlement_table 拆段后的键账变化)。**执行时点=迭代 40 合回 master 之后、用户发令**(「gdstyle 存量债全量清偿」条目清点报告波 0 行同源);处迭代 40 与 41 之间的固定动作,完成即删本条目。
|
||||
|
||||
Reference in new issue
Block a user