diff --git a/data/attributes.json b/data/attributes.json new file mode 100644 index 0000000..60fcd1e --- /dev/null +++ b/data/attributes.json @@ -0,0 +1,6 @@ +{ + "cpu_limit": { "display_name": "运算力 cpu_limit", "base": 0.0, "soft": 20.0, "hard": 50.0, "combine": "add_int" }, + "move_speed": { "display_name": "移动速度 move_speed", "base": 200.0, "soft": 600.0, "hard": 800.0, "combine": "hybrid" }, + "cast_delay_mod": { "display_name": "施法延迟 cast_delay_mod", "base": 1.0, "soft": 0.1, "hard": 0.01, "combine": "inverse" }, + "hp_max": { "display_name": "最大生命 hp_max", "base": 100.0, "soft": 2000.0, "hard": 0.0, "combine": "hybrid" } +} diff --git a/scripts/autoloads/player_stats.gd b/scripts/autoloads/player_stats.gd index a903b3c..d60b502 100644 --- a/scripts/autoloads/player_stats.gd +++ b/scripts/autoloads/player_stats.gd @@ -17,12 +17,17 @@ var xp: int = 0 var level: int = 1 var xp_to_next: int = 10 # 升级所需 XP -# ── 战斗属性 ─────────────────────────────────────────── -var cpu_limit: int = 5 # 控制 MAX_OPS = cpu_limit * 40 -var armor: float = 0.0 -var resistance: float = 0.0 # 0.0~1.0 +# ── 属性(生效值:静态类型裸字段,读取端零开销)───────────── +# 由 _recompute_attrs() 经 AttributeFormula 从 base + _modifiers 算出,勿直接赋值 +var cpu_limit: int = 0 # 生效运算力(含法杖 "core" 加成);MAX_OPS = 本值 × 40 +var move_speed: float = 200.0 # 像素/秒 +var cast_delay_mod: float = 1.0 # 施法间隔乘算系数,越低越快 var _invuln_until_msec: int = 0 # < now 表示可受击;受击后设为 now + iframe 窗口 +# 属性定义与加成来源(非热路径) +var _attr_def: Dictionary = {} # attributes.json 全量定义,只读 +var _modifiers: Array[Dictionary] = [] # [{attr_id, mode, value, source}] + # ── 魔力(MVP)───────────────────────────────────────── var mana: float = 100.0 var mana_max: float = 100.0 @@ -31,6 +36,8 @@ var mana_heat: float = 0.0 # 递增蓝耗热值(0=无,1=蓝耗翻倍) var mana_leech: float = 0.0 # 击杀回蓝量(换杖/换牌重算) func _ready() -> void: + _load_attr_definitions() + _recompute_attrs() EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed) func _on_enemy_killed(_payload: Dictionary) -> void: @@ -132,15 +139,70 @@ func reset_for_run() -> void: _invuln_until_msec = 0 stats_changed.emit() +## hp_max / cpu_limit 不入存档:二者现为 attributes.json + _modifiers 的派生值, +## 回读会覆盖公式结果。旧存档里的同名键忽略即可,无需提升 schema 版本。 func get_save_data() -> Dictionary: - return {"hp": hp, "hp_max": hp_max, "gold": gold, "xp": xp, "level": level, "cpu_limit": cpu_limit} + return {"hp": hp, "gold": gold, "xp": xp, "level": level} func load_save_data(data: Dictionary) -> void: - hp = float(data.get("hp", 100.0)) - hp_max = float(data.get("hp_max", 100.0)) + hp = float(data.get("hp", 100.0)) gold = int(data.get("gold", 0)) xp = int(data.get("xp", 0)) level = int(data.get("level", 1)) - cpu_limit = int(data.get("cpu_limit", 5)) xp_to_next = xp_for_level(level) stats_changed.emit() + +# ── 属性框架 ────────────────────────────────────────────── +const _ATTR_PATH: String = "res://data/attributes.json" + +func _load_attr_definitions() -> void: + if not FileAccess.file_exists(_ATTR_PATH): + push_error("PlayerStats: 缺少 %s" % _ATTR_PATH) + return + var parsed = JSON.parse_string(FileAccess.get_file_as_string(_ATTR_PATH)) + if not (parsed is Dictionary): + push_error("PlayerStats: %s 格式错误(应为对象)" % _ATTR_PATH) + return + _attr_def = parsed + +## 追加一条加成来源;同一 source 可对多个属性各加一条 +func add_modifier(attr_id: String, mode: String, value: float, source: String) -> void: + _modifiers.append({"attr_id": attr_id, "mode": mode, "value": value, "source": source}) + _recompute_attrs() + +## 撤销某来源的全部加成(换杖、出售退款用) +func remove_modifiers_from(source: String) -> void: + var kept: Array[Dictionary] = [] + for m in _modifiers: + if String(m.get("source", "")) != source: + kept.append(m) + _modifiers = kept + _recompute_attrs() + +func _mods_for(attr_id: String) -> Array: + var out: Array = [] + for m in _modifiers: + if String(m.get("attr_id", "")) == attr_id: + out.append(m) + return out + +## 逐属性按各自 combine 公式重算生效值,写回裸字段 +## 留给货架 C:一旦玩家可购买属性,需要持久化的是 _modifiers(来源列表)而非 +## 派生的生效值,届时 get_save_data 应加 "attr_modifiers": _modifiers 并提升 +## schema 版本。source == "core" 的那条**不要**持久化——它在换杖时由 +## combat_manager._rebuild_wand 重建。 +func _recompute_attrs() -> void: + if _attr_def.is_empty(): + return + cpu_limit = int(_compute_attr("cpu_limit")) + move_speed = _compute_attr("move_speed") + cast_delay_mod = _compute_attr("cast_delay_mod") + hp_max = _compute_attr("hp_max") + stats_changed.emit() + +func _compute_attr(attr_id: String) -> float: + var d: Dictionary = _attr_def.get(attr_id, {}) + if d.is_empty(): + push_error("PlayerStats: attributes.json 缺少属性「%s」" % attr_id) + return 0.0 + return AttributeFormula.compute(float(d.get("base", 0.0)), _mods_for(attr_id), d)