279 lines
12 KiB
GDScript
279 lines
12 KiB
GDScript
## 主角实体账:冒险生命周期主角数值与获得记录唯一所有者(冒险域拆件批自 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
|