Files
gplan-2/game/presentation/playtest_logger.gd
T
DKChan 5f9d8e7b77 chore(代码卫生): 文件头零工作日志红线落文+全仓头注变更叙事清偿(2026-10-15 用户令)
- CLAUDE.md 硬性红线新增「文件头零工作日志」;code-conventions.md 新增 §1.4.1(含反面案例)
- 全仓 .gd 文件头注清偿:删除全部带日期条目、批次叙事段与迁移退役叙述,保留并改写为
  现在时的职责、接口契约、GDD 锚内容(main.gd 头注 42→31 行、save_manager.gd 49→45 行)
- 纯注释改动零行为变更,按用户令免测试免冒烟
2026-10-15 04:08:38 +08:00

185 lines
7.8 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.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(§Events Fired 遥测义务——ux 行族)。
# Date: 2026-09-12(战斗屏 ux 遥测)
# 试玩日志器(呈现层组件——订阅核心信号持久化 JSONL,供数值分析;卡死诊断转储)。
# 职责边界:核心层只发 settlement_snapshot 信号(战斗结算)/既有信号(battle_finished/node_entered/
# level_finished/step_row);本组件负责订阅、缓冲与写文件——核心零日志感知(零 UI 依赖不变)。
# 文件 = user://logs/playtest_<seed>_<时间戳>.jsonl(每行一次结算/终局/节点的全量状态 JSON)。
# ux 行族(呈现层观测,不改 core):ux_round_input_ratio(原始输入动作数∶AP 消耗)、
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 战斗结算 的 ADR 纪律下唯一合法来源)、
# ux_refresh_attempt / ux_reject / ux_hint_shown(组合根显式上报;无 ux_window_choice——道具窗口已下线)。
# 诊断转储 = 全量核心状态 + 最近结算/操作环形缓冲 → user://logs/diag_<时间戳>.json(卡死时点按钮)。
class_name PlaytestLogger
extends RefCounted
const RING_SIZE := 200
var _file: FileAccess = null
var _path := ""
var _seed := 0
var _recent_steps: Array[Dictionary] = [] # 环形缓冲:最近结算行
var _recent_actions: Array[Dictionary] = [] # 环形缓冲:最近玩家动作
var _snap_count := 0
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号)
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
# 开启会话(每次 start_level/start_battle 调用——按种子分文件)。返回日志路径。
func start_session(seed_val: int) -> String:
_seed = seed_val
_recent_steps.clear()
_recent_actions.clear()
_snap_count = 0
DirAccess.make_dir_recursive_absolute("user://logs")
var stamp: String = Time.get_datetime_string_from_system().replace(":", "").replace("-", "").replace(" ", "_")
_path = "user://logs/playtest_%d_%s.jsonl" % [seed_val, stamp]
if _file != null:
_file.close()
_file = FileAccess.open(_path, FileAccess.WRITE)
_append({"kind": "session", "seed": seed_val, "ts": Time.get_datetime_string_from_system()})
return _path
func close_session() -> void:
if _file != null:
_file.close()
_file = null
func log_path() -> String:
return _path
func attach_battle(settlement: BattleOrchestrator) -> void:
settlement.step_row.connect(_on_step_row)
settlement.settlement_snapshot.connect(_on_settlement_snapshot)
settlement.battle_finished.connect(_on_battle_finished_log)
func _on_battle_finished_log(p: Dictionary) -> void:
_append({
"kind": "battle_end", "result": String(p.get("result", "")),
"hero_hp": int(p.get("hero_hp", 0)),
"gp_dropped": int(p.get("gp_dropped", 0))}) # 战斗掉落列(GP+商店房实施批 2026-09-21——验收取数面)
# ---- GP 钱包行(GP+商店房实施批——到账/购买/清零事件;runner wallet_changed 投影经组合根转发)----
func log_wallet(event: String, gp: int, extra: Dictionary = {}) -> void:
var line := {"kind": "wallet", "event": event, "gp": gp}
for k in extra.keys():
line[k] = extra[k]
_append(line)
# ---- ux 遥测(battle.md §Events Fired——呈现层观测行族,不改 core) ----
func log_ux(ux_kind: String, payload: Dictionary) -> void:
var line := {"kind": "ux", "ux": ux_kind}
for k in payload.keys():
line[k] = payload[k]
_append(line)
func log_action(kind: String, cell: Vector2i, accepted: bool, extra: Dictionary = {}) -> void:
_ux_inputs += 1 # 原始输入动作数(含被拒——P4 ≤2 护栏观测口径 = 原始输入)
# 逐操作落盘(2026-09-12 用户要求:操作记录完整持久化——此前仅入环形缓冲、卡死诊断才可见)
var line := {"kind": "action", "op": kind, "cell": [cell.x, cell.y], "accepted": accepted}
for k in extra.keys():
line[k] = extra[k]
_append(line)
_ring(_recent_actions, {"kind": kind, "cell": [cell.x, cell.y], "accepted": accepted}.merged(extra))
func _on_settlement_snapshot(snap: Dictionary) -> void:
_append(snap)
# ux_round_exit{path_tag}:对齐上游封闭枚举 {stopped, ap_exhausted, cleared}
var facts: Dictionary[Variant, Variant] = snap.get("round_facts", {})
log_ux("ux_round_exit", {"path_tag": String(facts.get("path", ""))})
# ux_round_input_ratio:原始输入动作数∶AP 消耗(本轮 AP 配额 − 快照时剩余 = 消耗)
var economy: Dictionary[Variant, Variant] = snap.get("economy", {})
var ap_left: int = int(economy.get("ap", 0))
var consumed: int = clampi(ap_per_round - ap_left, 0, ap_per_round)
log_ux("ux_round_input_ratio", {"inputs": _ux_inputs, "ap_consumed": consumed,
"ratio": float(_ux_inputs) / float(maxi(consumed, 1))})
_ux_inputs = 0
func log_node(node: Dictionary, info: Dictionary, node_index: int) -> void:
var t := String(node.get("type", ""))
var line := {"kind": "node", "type": t, "index": node_index}
if t == "battle" or t == "boss":
line["depth"] = int(node.get("node_depth", 0))
elif t == "rest":
line["healed"] = int(info.get("healed", 0))
elif t == "shop":
line["gp"] = int(info.get("gp", 0)) # 进店时点余额(GP+商店房实施批)
# reward 节点:内容待拍板(格面资源批 2026-09-15 道具删除)——零载荷
_append(line)
# 批量模拟会话的局次标记(一文件多局——run_start 行携带该局种子/档位,供逐局切分)
func log_run(meta: Dictionary) -> void:
var line := {"kind": "run"}
for k in meta.keys():
line[k] = meta[k]
_append(line)
func log_level_end(payload: Dictionary) -> void:
_append({"kind": "level_end", "result": String(payload.get("result", "")),
"node_index": int(payload.get("node_index", -1)), "hp": int(payload.get("hp", 0))})
func _on_step_row(row: Dictionary) -> void:
_ring(_recent_steps, {"t": row.type, "src": row.source_class, "v": int(row.value), "r": int(row.round_index)})
# 结算明细行逐行落盘(2026-09-12 用户要求——五段结算完整回放数据面);
# 公共七字段之外的全部行字段(payload:roll/ac/tier/crit/normal/pierce/magic/baseline/rider/target/
# effect_id/pool_value 等)随行平铺落盘(2026-09-17 数值分析工具批——无 payload 的旧日志反事实只能
# 比例近似,新日志可精确重算)。
var line := {"kind": "step", "type": row.type, "source": row.source_class,
"cell": row.origin_cell, "round": int(row.round_index),
"text": String(row.display_name), "value": int(row.value),
"highlight": String(row.get("highlight", ""))}
for k in row.keys():
if not line.has(k):
line[k] = row[k]
_append(line)
# ---- 诊断转储(卡死时点击——全量状态 + 环形缓冲)----
func dump_diagnosis(context: Dictionary) -> String:
DirAccess.make_dir_recursive_absolute("user://logs")
var stamp: String = Time.get_datetime_string_from_system().replace(":", "").replace("-", "").replace(" ", "_")
var path := "user://logs/diag_%s.json" % stamp
var dump := {
"kind": "diagnosis", "ts": Time.get_datetime_string_from_system(), "seed": _seed,
"session_log": _path, "snap_count": _snap_count,
"recent_actions": _recent_actions.duplicate(),
"recent_steps": _recent_steps.duplicate(),
"core": context,
}
var f := FileAccess.open(path, FileAccess.WRITE)
if f != null:
f.store_string(JSON.stringify(dump, " "))
f.close()
print("[playtest_logger] 诊断转储已写入: %s(项目日志目录见输出)" % path)
print(" [diag] %s" % JSON.stringify(context))
return path
func _ring(buf: Array, item: Dictionary) -> void:
buf.append(item)
while buf.size() > RING_SIZE:
buf.pop_front()
func _append(dict: Dictionary) -> void:
if dict.get("kind", "") == "settlement":
_snap_count += 1
if _file == null:
return
_file.store_line(JSON.stringify(dict))
_file.flush()