合并(迭代39拆件批): 三卡实现(主卡冒险域拆件与账本实体化/副卡同调配装充能域封装/触发器卡技能池情境条件触发器)+领域拆分v1.3(B1主角账三件HeroStats/GpWallet/GainJournal·B2停点刷新账RefreshBank·B3私有助手抽取·B4战斗指标面)——特批带红合并: 02 EV锚(goblin 0.9148∉[1.00,1.20],重锚归后续数值批待用户发起)+08d 生涯.md:55行号漂移(修复归合并后文档批,反哺点在册)登记存量红;全量闸回执验收通过(闸况与基线一致,09指纹回归91d45583cf6d);断点对拍292dumps×28键mismatch=0
This commit is contained in:
commit
ffb0ac1526
73 files changed
+1755939
-2747
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
|
||||
@@ -67,12 +67,13 @@ var _combat_seg = null # CombatSegment(③战斗段)
|
||||
var _decay_seg = null # DecaySegment(④衰减段)
|
||||
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类)
|
||||
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
|
||||
var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账)
|
||||
# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
|
||||
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
|
||||
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
|
||||
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
|
||||
# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
|
||||
var attunement: Array[Variant] = [] # 装备中同调件 item_id 集
|
||||
var _ledger: RunLedger = null # 共享四本账(begin_battle 经 enc["ledger"] 持引用;缺省新账=调试面)
|
||||
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口)
|
||||
# ---- 效果引擎扩签运行态(新效果类型施加面;空订阅零扰动)----
|
||||
var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析)
|
||||
@@ -94,6 +95,18 @@ var fx_counters_battle: Dictionary[Variant, Variant] = {} # battle 桶(无前
|
||||
var counters_ctx: EffectContext = null # 四桶引用束(_init 建、begin_battle 挂 adventure/level 引用)
|
||||
|
||||
|
||||
# 加算日记读面(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)
|
||||
|
||||
|
||||
# 事件发射薄面(18 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
|
||||
func _fx_emit(event: String, payload: Dictionary) -> Array:
|
||||
if _fx == null:
|
||||
@@ -233,24 +246,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 区秘宝槽悬停浮窗进度行:
|
||||
@@ -322,10 +320,6 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
# 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
|
||||
# (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后);
|
||||
# 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。
|
||||
# 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
|
||||
# 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
|
||||
# (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后);
|
||||
# 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。
|
||||
# 逐行骰:混编行集**每行各掷自己的区间骰**——
|
||||
# 先按行序逐行数量骰、再按行序逐行 atk 骰(单行行集 = 既有一次数量骰 + 一次 atk 骰,判定条件、
|
||||
# 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」
|
||||
@@ -369,14 +363,11 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
if not _install_new_board():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
|
||||
# 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入
|
||||
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
|
||||
# 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
|
||||
attunement = (enc.get("attunement", []) as Array).duplicate()
|
||||
_attune_safe_seen = 0
|
||||
# 淬毒/偏运=triggers 订阅行(淬毒=clamp_check
|
||||
# modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,begin_battle 不注入)
|
||||
# 特性运行态(enc.feats 携带持有集:效果行为=数据行 triggers 经效果
|
||||
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
|
||||
# 淬毒/偏运=triggers 订阅行(淬毒=clamp_check modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,begin_battle 不注入)
|
||||
# 特性运行态(enc.feats 携带持有集:效果行为=数据行 triggers 经效果引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
|
||||
# 扩签运行态就位:AC 基线快照、累积器清零、adventure 计数器/加算区共享引用接收
|
||||
_fx_hero_ac_base = int(enc.get("hero_armor_class", -1))
|
||||
_fx_ac_round = 0
|
||||
@@ -384,9 +375,8 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_fx_new_resource_types = {}
|
||||
_fx_pending_reveals = []
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
var ledger_ref = enc.get("ledger", null)
|
||||
_ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面
|
||||
counters_ctx.adventure_bucket = _ledger.adventure_counters # 挂冒险桶引用(跨战持久=同一字典引用,非副本搬运)
|
||||
_mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款;装备/日记读面经 encounter 派生——无成员接收行)
|
||||
counters_ctx.adventure_bucket = enc.get("adv_bucket", null) if enc.get("adv_bucket", null) is Dictionary else {} # 冒险桶宿主字典(直构兜底自有空字典)
|
||||
var level_ref: Variant = enc.get("level_counters")
|
||||
if level_ref is Dictionary:
|
||||
counters_ctx.level_bucket = level_ref # 挂关卡桶引用(LevelRunner 每关 new 下传)
|
||||
@@ -398,35 +388,29 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_damage.rider_tier_bonus = 0
|
||||
_damage.rider_tier_crit = false
|
||||
_feat_first_dig_used = false
|
||||
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
|
||||
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
|
||||
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
|
||||
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。
|
||||
# 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露))
|
||||
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
|
||||
# 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;
|
||||
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。
|
||||
# 计数器四桶经 counters_ctx 引用宿主容器,无回灌时点(旧战首回灌时点删除))
|
||||
# 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;树规则合成订阅同面装载。
|
||||
# 装载序=feats 表序→treasure 表序→树规则,写死保种子复放。计数器四桶经 counters_ctx 引用宿主容器,无回灌时点(旧战首回灌时点删除))
|
||||
_fx.rebuild(feat_ids, attunement, tree_rules)
|
||||
# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;
|
||||
# 缺省 0/false=零装配零扰动。注入在首板 install 之后〔板在早段〕——板生成不读两参数,
|
||||
# 参数只影响挖掘/插棋动作判据,时序安全)
|
||||
# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;缺省 0/false=零装配零扰动。
|
||||
# 注入在首板 install 之后〔板在早段〕——板生成不读两参数,参数只影响挖掘/插棋动作判据,时序安全)
|
||||
minesweeper.dig_range_bonus = attune_param_sum("dig_range_bonus")
|
||||
minesweeper.seal_range_free = attune_param_sum("seal_range_free") > 0
|
||||
# 充能重置周期(battle=每战回满/level=每关首战回满〔battle_index==1〕;
|
||||
# adventure 周期维持冒险开始 _seed_attune_charges 单次)
|
||||
# 充能重置周期(battle=每战回满/level=每关首战回满〔battle_index==1〕;adventure 周期维持冒险开始种行单次)
|
||||
_reseed_attune_charges("battle")
|
||||
if int(enc.get("battle_index", 1)) == 1:
|
||||
_reseed_attune_charges("level")
|
||||
# board_installed 发射+施加(巢穴预告:装板后一次报本板危险格数量;
|
||||
# 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归
|
||||
# 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载)
|
||||
# board_installed 发射+施加(巢穴预告:装板后一次报本板危险格数量;只报数量不标位置=信息红线,
|
||||
# 不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载)
|
||||
nest_preview_count = -1
|
||||
var bi_hits := _fx_emit("board_installed", {})
|
||||
for h in bi_hits:
|
||||
if String(h.get("kind", "")) == "mine_count_preview":
|
||||
nest_preview_count = minesweeper.mine_total()
|
||||
# 跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
|
||||
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
|
||||
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
|
||||
# 跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现「每场战斗开始首轮 AP +加成值」
|
||||
#(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
|
||||
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) + int(enc.get("voucher_time_sand", 0)) # 时沙同通道(首轮池增量,不改上限)
|
||||
if ap_bonus > 0:
|
||||
action_economy.queue_ap_delta(ap_bonus)
|
||||
@@ -434,11 +418,8 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
var voucher_rap: int = int(enc.get("voucher_rap", 0))
|
||||
if voucher_rap > 0:
|
||||
action_economy.grant_reserve_ap(voucher_rap)
|
||||
# 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载,
|
||||
# when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现
|
||||
# round_start 发射+施加(发射位先于行动经济轮切换:树规则 grant_ap
|
||||
# mode:queue 当轮入队、notify_round_start 即兑现(先手压制=首轮 AP+1 语义锚);
|
||||
# 计数器 round_start 重置点同点生效)
|
||||
# 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载,when round_index ≤ effective_rounds 行字段 value_from 定影;
|
||||
# 发射位先于轮切换=当轮兑现(树规则 grant_ap mode:queue 当轮入队、notify_round_start 即兑现——先手压制=首轮 AP+1 语义锚;计数器 round_start 重置点同点生效)
|
||||
round_index = 1
|
||||
_emit_round_start()
|
||||
if not action_economy.notify_round_start():
|
||||
@@ -759,14 +740,14 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
|
||||
if blasted.is_empty():
|
||||
return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"}
|
||||
var rec: Dictionary[Variant, Variant] = treasure_inventory.consume(handle)
|
||||
# 同调主动件充能扣减(共享账本直改:adventure/level/settlement 同一字典引用)
|
||||
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement":
|
||||
_ledger.attune_charge_consume(String(rec.get("item_id", "")))
|
||||
# 同调主动件充能消耗(单源化——行标 used 幂等承载,原字典扣减同点退役)
|
||||
if _attune_loadout() != null and not rec.is_empty() and String(rec.get("source_class", "")) == "attunement":
|
||||
_attune_loadout().consume_charge(String(rec.get("item_id", "")), treasure_inventory)
|
||||
# 真视类工具使用计数(信标网络解锁行为条件:受理成功恰计一次;
|
||||
# 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used)
|
||||
# 主角账单点记录,冒险收束聚合入档 save.add_vision_used)
|
||||
if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"):
|
||||
if not _ledger.vision_counter.is_empty():
|
||||
_ledger.vision_count_add(1) # 受理成功恰计一次(缺省空账=调试不计)
|
||||
if _journal() != null:
|
||||
_journal().record_vision_use() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义)
|
||||
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
|
||||
|
||||
|
||||
@@ -856,6 +837,14 @@ func _apply_gp_row(row: Dictionary, source_class: String, cell: Vector2i, ri: in
|
||||
# pierce/magic 正向 = 单目标克制直伤单据(attack_type 查克制矩阵,必中全额不掷点);负向 = 主角直伤单据。
|
||||
# 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(行已自记)——读数 = 控制器结账 outcome(A 模式提交即结)。
|
||||
# sync = 同步开单(清图批——流程依赖完美第二份/高光份数读数,无视结账政策当场结账)。
|
||||
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励同走 blast 分支不计入)
|
||||
func _book_negative_blast(row: Dictionary) -> void:
|
||||
if String(row.get("polarity", "positive")) != "negative":
|
||||
return
|
||||
_mines_hits["round"] += 1
|
||||
_mines_hits["total"] += 1
|
||||
|
||||
|
||||
func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", sync := false) -> String:
|
||||
var ctype := String(row.cell_type)
|
||||
var power := int(row.power)
|
||||
@@ -866,6 +855,7 @@ func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri:
|
||||
_emit_row("effect", source_class, cell, ri, "%s(下轮行动点 %+d)" % [display, delta], delta, {"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()}, highlight)
|
||||
return "redeemed"
|
||||
if ctype == "blast":
|
||||
_book_negative_blast(row) # 负向 blast(雷)踩雷计数——正向 spark_aoe/清图奖励同走此分支不计入
|
||||
var t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, source_class, display, "board", power, \
|
||||
DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {"batch_key": String(row.reward_id)})
|
||||
return _outcome_of(_submit(t, sync))
|
||||
@@ -964,22 +954,31 @@ func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool:
|
||||
# 摇定后、兑现前死亡 → 本拍技能作废。
|
||||
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("highlight", "enemy_action", Vector2i(-1, -1), round_index,
|
||||
"%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
|
||||
0, {"skill_id": sid}, "skill_unlock")
|
||||
# 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)
|
||||
@@ -988,14 +987,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
|
||||
|
||||
|
||||
# ---- 战斗段 ----
|
||||
@@ -1127,13 +1128,12 @@ 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 战末汇入(血税双向账:本战累积并入冒险加算区共享引用后清零)+
|
||||
# adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例)
|
||||
if not _ledger.bonus.is_empty():
|
||||
# bonus_delta 战末汇入(血税双向账:本战累积经加算日记单写原语汇入获得日记三层后清零)
|
||||
if _journal() != null:
|
||||
for axis in _fx_bonus_delta.keys():
|
||||
_ledger.bonus_add_axis(axis, int(_fx_bonus_delta[axis]))
|
||||
_journal().gain_bonus(String(axis), int(_fx_bonus_delta[axis]), "battle_export", String(axis))
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
# 搬运链删除:adventure 桶经 ctx 引用直写 RunLedger,无战末导出时点(旧导出/汇入搬运链删除)
|
||||
# 搬运链删除:adventure 桶经 ctx 引用直写宿主字典,无战末导出时点(旧导出/汇入搬运链删除)
|
||||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 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
|
||||
@@ -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:
|
||||
|
||||
@@ -416,7 +416,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 转正随修;头部校验串本体零触碰)
|
||||
@@ -434,10 +434,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:
|
||||
@@ -604,7 +604,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:
|
||||
@@ -713,7 +713,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))
|
||||
|
||||
@@ -56,6 +56,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——排空零分叉/零丢行/延迟双态
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ehnthaqk7h12
|
||||
@@ -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.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,216 @@
|
||||
# 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)不递增——负向计数守卫
|
||||
orch._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
|
||||
"display_name": "爆裂", "reward_id": "spark_aoe"}, "reward", Vector2i(-1, -1), 1)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0,
|
||||
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
|
||||
# 负向 blast(雷)递增
|
||||
orch._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
|
||||
"display_name": "警觉", "reward_id": "enemy_alert"}, "reward", Vector2i(-1, -1), 1)
|
||||
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,
|
||||
|
||||
@@ -117,9 +117,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 逐位同旧)
|
||||
@@ -219,7 +219,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)")
|
||||
|
||||
@@ -269,13 +270,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
|
||||
@@ -283,7 +283,7 @@ func _init() -> void:
|
||||
var tgt13: Vector2i = _first_closed_safe(ob13)
|
||||
if tgt13 != Vector2i(-9, -9):
|
||||
ob13.use_treasure(h13, tgt13)
|
||||
Kit.check_quiet(int(lg13.vision_counter.get("used", 0)) == 1,
|
||||
Kit.check_quiet(lg13.vision_used() == 1,
|
||||
"计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)")
|
||||
|
||||
# ⑭ 装配不可变(V6:冒险全程装配快照三面同源且零写口——
|
||||
@@ -306,7 +306,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 同款)。
|
||||
Reference in new issue
Block a user