## 主角实体账:冒险生命周期主角数值与获得记录唯一所有者(冒险域拆件批自 AdventureRunner 迁入)。 ## 生命与 GP 钱包(gain/spend/clear 单写原语);锻造强化快照(永久强化 flat)与调试浮窗百分比通道; ## 攻击/甲类型轴快照;特性持有集(活引用注入——关内兑付当关生效);本局钥石增量与逐杀明细; ## 真视使用记录;统一冒险加算区(商店购买/三选一属性件/战末导出三通道唯一落点)。 ## 获得记录三层结构=获得记录列表(record_gain 追加、返回记录 id)→ 属性增益明细(带来源指回)→ ## 四轴汇总(由明细同步维护;summary 与断点读汇总,日记为正主)。 ## 生效上限/攻击/护甲公式与回血封顶公式单点归本件(与战斗内上限同式同源,pct=0 时乘法恒等——MC 锚零漂移)。 ## Core 层零 UI;随机零消费;断点自交片段(账本键+获得日记键)——拼盘落盘归冒险应用层。 class_name HeroLedger extends RefCounted # 加算区四轴(键序即断点 bonus 键序——与现役 run_ledger.bonus 置账形态逐位同形) const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"] var db: ConfigDb var hero_hp: int = 0 # 生命账(原冒险账本 ledger["hero_hp"] 单值化) var gp: int = 0 # GP 钱包(原 ledger["gp"]——LevelRunner 与 ShopSession 读写全改指此单点) 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 应用面) var _gain_log: Array[Variant] = [] # 获得记录列表(三选一件/商店购买/战末奖励各一条) var _gain_seq := 0 # 获得记录 id 发号(1 起) 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 → 已购档数(递增定价输入;冒险开始清零) func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void: db = db_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] func debug_atk_pct() -> int: return int(_override[0]) func debug_hp_pct() -> int: return int(_override[1]) # 永久强化 flat 快照读面(续玩生命钳制保守上限用——不含加算区) func base_override_flat() -> Dictionary[Variant, Variant]: return _base_override.duplicate(true) # 注入参数规范化:三字段缺键/-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)) + 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)) + 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)) + 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) # ---- GP 钱包单写原语 ---- 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 # 单关模式重开重置(值重置非实例重建——shop 绑 hero 对象引用不换代;语义=原单关 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: gp = 0 _bonus_total = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} _gain_log = [] _gain_seq = 0 _bonus_detail = [] _purchased = {} _keystone = 0 _keystone_drops = [] _vision_used = 0 feats = [] hero_hp = effective_max_hp() # ---- 获得记录三层 ---- # 获得记录列表追加(三通道各一条——source_class 词表:curio_pick/shop_product/battle_export; # 返回记录 id 供明细来源指回) func record_gain(source_class: String, kind: String, detail: Dictionary) -> int: _gain_seq += 1 var rec: Dictionary[Variant, Variant] = {"id": _gain_seq, "source_class": source_class, "kind": kind} rec.merge(detail.duplicate(true)) _gain_log.append(rec) return _gain_seq # 统一冒险加算区单写原语(三通道统一入口:三选一属性件 source_class="curio_pick"; # 商店属性商品 source_class="shop_product"、kind=product_id;战末导出 source_class="battle_export"、 # kind=轴名。明细+汇总同步维护——写时同拍,无事后对账) 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)) func purchased_snapshot() -> Dictionary[Variant, Variant]: return _purchased.duplicate(true) # 本局钥石增量(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 # 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) func feats_held() -> Array: return feats.duplicate() # ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)---- # 片段键:hero_hp/bonus(汇总快照)/gp/shop_purchased/keystone/keystone_drops/relics/vision_counter_used # +获得日记键(唯一声明的断点扩列;旧档缺日记→汇总直载跳过比对;日记在档→由日记推导汇总比对) func breakpoint_fragment() -> Dictionary[Variant, Variant]: return { "hero_hp": hero_hp, "bonus": bonus_snapshot(), "gp": gp, "shop_purchased": purchased_snapshot(), "keystone": _keystone, "keystone_drops": keystone_drop_log(), "relics": feats.duplicate(), "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: hero_hp = int(frag.get("hero_hp", 0)) gp = int(frag.get("gp", 0)) _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) feats = [] for r in (frag.get("relics", []) as Array): feats.append(String(r)) _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) _gain_seq = _gain_log.size() for rec in _gain_log: _gain_seq = maxi(_gain_seq, int((rec as Dictionary).get("id", 0))) _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