Files
gplan-2/game/core/hero_ledger.gd
T

279 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 主角实体账:冒险生命周期主角数值与获得记录唯一所有者(冒险域拆件批自 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