## 属性编辑器标签 — 玩家属性的 base / soft / hard / combine
## 数据：res://data/attributes.json（权威属性定义见 docs/design/numerical_design.md §1.1）
@tool
extends VBoxContainer

const UI = preload("res://addons/game_designer/designer_ui.gd")
const PATH = "res://data/attributes.json"
# 显示与存储解耦：LABELS 仅供下拉显示，KEYS 才写进 JSON，二者 index 对齐
const COMBINE_KEYS   = ["hybrid", "inverse", "add_int"]
const COMBINE_LABELS = ["混合 hybrid", "反向 inverse", "整数加 add_int"]
const ATTR_ORDER = ["cpu_limit", "move_speed", "cast_delay_mod", "hp_max"]

var _data: Dictionary = {}
var _rows: Dictionary = {}   # attr_id → {"base": SpinBox, "soft": SpinBox, "hard": SpinBox, "combine": OptionButton}
var _status: Label
var _loaded: bool = false   # 加载失败时禁止写回，见 _save

func _ready() -> void:
	add_child(UI.header("📊 属性编辑器 — 玩家属性 base / soft / hard / combine"))
	_load()
	var grid := GridContainer.new(); grid.columns = 5; add_child(grid)
	for h in ["属性", "基准 base", "软上限 soft", "硬上限 hard", "合成 combine"]:
		var l := Label.new(); l.text = h; l.modulate = Color(0.7, 0.8, 1.0)
		grid.add_child(l)
	for attr_id in ATTR_ORDER:
		# _load 已保证：_loaded 时每个条目都是 Dictionary，否则 _data 为空
		var d: Dictionary = _data.get(attr_id, {})
		grid.add_child(UI.cell_label(String(d.get("display_name", attr_id))))
		# 四个属性按定义均非负，故 min = 0；且 min 取 0 可避开 Range 的抵消误差（见 _clean）
		var sp_base := UI.spin(0.0, 99999.0, 0.01, float(d.get("base", 0.0)))
		var sp_soft := UI.spin(0.0, 99999.0, 0.01, float(d.get("soft", 0.0)))
		var sp_hard := UI.spin(0.0, 99999.0, 0.01, float(d.get("hard", 0.0)))
		var combine_key := String(d.get("combine", "hybrid"))
		var idx: int = COMBINE_KEYS.find(combine_key)
		# 本页凡是「静默改写权威数据」的路径都必须留下诊断，一条都不能漏——
		# 保存会把改写结果写回 attributes.json，而下游 PlayerStats 只会照单全收
		if d.is_empty():
			# 条目缺失时占位的 0 一旦被保存，键就「存在」了，
			# player_stats.gd 的「缺少属性」报错从此不再触发，hp_max=0 静默进游戏
			push_warning("attribute_tab: %s 缺少属性「%s」，本页以 0 值占位，保存会把 0 写进文件" % [PATH, attr_id])
		else:
			if idx < 0:
				# 不硬拒（打字错误不该让整页打不开），但必须留下诊断：保存会把它改写成 hybrid
				push_warning("attribute_tab: 「%s」的 combine「%s」无法识别，下拉已回落到 hybrid，保存将覆盖原值" % [attr_id, combine_key])
			for f in ["base", "soft", "hard"]:
				if d.has(f) and float(d[f]) < 0.0:
					push_warning("attribute_tab: 「%s」的 %s = %s 为负，输入框下限 0 已将其钳制，保存会把 0 写进文件" % [attr_id, f, str(d[f])])
		var op := UI.opt(COMBINE_LABELS, idx if idx >= 0 else 0)
		grid.add_child(sp_base); grid.add_child(sp_soft); grid.add_child(sp_hard); grid.add_child(op)
		_rows[attr_id] = {"base": sp_base, "soft": sp_soft, "hard": sp_hard, "combine": op}
	add_child(HSeparator.new())
	var tip := Label.new()
	tip.text = "hard=0 在 hybrid / add_int 下表示不钳制；inverse 的 hard 是「下限」，填 0 即钳到 0（与 hybrid 相反）；add_int 拒绝 pct 加成；soft 本期不参与计算（留给货架 C）。"
	# dock 的 ScrollContainer 禁用横向滚动，不换行的话这句会被右侧裁掉（正是最需要看到的半句）
	tip.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	tip.modulate = Color(0.7, 0.7, 0.7)
	add_child(tip)
	add_child(UI.btn("💾 保存 attributes.json", _save))
	_status = UI.status_label(); add_child(_status)

## 加载失败必须留痕并禁止写回：本页的零值载荷（move_speed.base = 0 等）是合法 JSON，
## PlayerStats 会照单全收，游戏表现为玩家不能移动 / 瞬间死亡且毫无诊断。
func _load() -> void:
	var d = UI.load_json(PATH)
	_loaded = d is Dictionary
	if not _loaded:
		push_error("attribute_tab: 无法加载 %s（缺失或格式错误）" % PATH)
		_data = {}
		return
	# 逐条守卫，同 player_stats.gd 的既有做法：畸形条目（如 "hp_max": 5）会让下面的
	# Dictionary 赋值硬崩、dock 停在半构建状态；整个文件拒绝加载并禁止写回
	for k in d:
		if not (d[k] is Dictionary):
			push_error("attribute_tab: %s 的「%s」应为对象，整个文件已拒绝加载" % [PATH, k])
			_loaded = false
			_data = {}
			return
	_data = d

## Range 用 round((v - min) / step) * step + min 吸附取值。min = -99999 与 0.01 的 step
## 相差七个数量级，此式在此发生抵消误差（0.01 → 0.00999999999476），直接写回会把噪声
## 固化进 JSON。本页 min = 0.0 时 12 个值原本就逐位精确，本函数当前不改变落盘结果
##（唯一副作用：0.1 在内存里低 1 ULP，stringify 仍输出 0.1）。
## 勿因此删除 —— 一旦某属性需要负值 / 宽区间而放宽 min，抵消误差立即回来。
static func _clean(v: float) -> float:
	return snappedf(v, 0.000001)

## 表单 → 字典（合并式：以 _data 为基底保留未知属性条目，以及条目内未列入表单的键；
## 顶层标量键不在此列——_load 已因它们整体拒绝加载）
func _collect() -> Dictionary:
	var out: Dictionary = _data.duplicate(true)
	for attr_id in ATTR_ORDER:
		var r: Dictionary = _rows.get(attr_id, {})
		if r.is_empty():
			continue
		var entry: Dictionary = out.get(attr_id, {}).duplicate(true)
		entry["base"]    = _clean(r["base"].value)
		entry["soft"]    = _clean(r["soft"].value)
		entry["hard"]    = _clean(r["hard"].value)
		# 存 key，不存双语显示串；selected 恒为 0..2（UI.opt 构造时已 clampi，本页此后不再赋值）
		entry["combine"] = COMBINE_KEYS[r["combine"].selected]
		out[attr_id] = entry
	return out

func _save() -> void:
	if not _loaded:
		UI.set_status(_status, "✗ 定义未加载，拒绝写回", true)
		return
	if UI.save_json(PATH, _collect()):
		UI.set_status(_status, "💾 已保存（重启 F5 生效）")
	else:
		UI.set_status(_status, "✗ 保存失败", true)
