-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathgamescriptflow.py
More file actions
197 lines (167 loc) · 7.02 KB
/
gamescriptflow.py
File metadata and controls
197 lines (167 loc) · 7.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# -*- coding: utf-8 -*-
"""脚本文档聚合模型:段落、连接、变量。"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from dataclasses import asdict, dataclass, field
from ScriptFlowEditor.models.flag import FlagType, FlagVariable
from ScriptFlowEditor.models.segment import StorySegment
from ScriptFlowEditor.models.path import SegmentPath
@dataclass
class GameScriptFlow:
"""
游戏脚本流程。
聚合所有剧情段落、段落间分支路径、以及分支路径条件相关变量(flag),
构成一份完整的游戏脚本流程数据。
"""
# 脚本名称,用于在编辑器中显示与引用。
name: str = ""
# 剧情段落(节点)列表。
segments: list[StorySegment] = field(default_factory=list)
# 段落间分支路径列表。
paths: list[SegmentPath] = field(default_factory=list)
# 分支路径条件相关变量列表。
flags: list[FlagVariable] = field(default_factory=list)
# 脚本流程标题(可选,用于显示)。
title: str = ""
# 注释。
comment: str | None = None
# 唯一标识,由 name 哈希生成 8 位;不传则自动生成。
id: str | None = None
def __post_init__(self) -> None:
if self.id is None:
self.id = hashlib.sha256(self.name.encode()).hexdigest()[:8]
# 按 id 查找剧情段落。
def get_segment_by_id(self, segment_id: str) -> StorySegment | None:
for s in self.segments:
if s.id == segment_id:
return s
return None
# 按 id 查找变量。
def get_flag_by_id(self, flag_id: str) -> FlagVariable | None:
for f in self.flags:
if f.id == flag_id:
return f
return None
# 按名称查找变量。
def get_flag_by_name(self, name: str) -> FlagVariable | None:
for f in self.flags:
if f.name == name:
return f
return None
# 按前置段落 id 查找分支路径。
def get_path_prev(self, prev_segment_id: str) -> list[SegmentPath]:
return [c for c in self.paths if c.prev_segment_id == prev_segment_id]
# 按后续段落 id 查找分支路径。
def get_path_next(self, next_segment_id: str) -> list[SegmentPath]:
return [c for c in self.paths if c.next_segment_id == next_segment_id]
# 序列化为可 JSON 序列化的字典(JSON 对象)。
def to_dict(self) -> dict:
return {
"name": self.name,
"id": self.id,
"title": self.title,
"comment": self.comment,
"segments": [asdict(s) for s in self.segments],
"paths": [asdict(p) for p in self.paths],
"flags": [self._flag_to_dict(f) for f in self.flags],
}
# 将 FlagVariable 转为 dict,flag_type 枚举转为字符串。
@staticmethod
def _flag_to_dict(f: FlagVariable) -> dict:
d = asdict(f)
if hasattr(f.flag_type, "value"):
d["flag_type"] = f.flag_type.value
return d
# 序列化为 JSON 字符串。
def to_json(self, *, indent: int | None = None, ensure_ascii: bool = False) -> str:
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=ensure_ascii)
def _get_generated_dir(self) -> Path:
"""返回 ScriptFlowEditor/generated 目录路径,不存在则创建。"""
# 本文件位于 ScriptFlowEditor/models/gamescriptflow.py
generated = Path(__file__).resolve().parent.parent / "generated"
generated.mkdir(parents=True, exist_ok=True)
return generated
@classmethod
def load_from_json(cls, path: str | Path) -> GameScriptFlow:
"""
从 JSON 文件反序列化为 GameScriptFlow 对象。
:param path: JSON 文件路径,例如 src/ScriptFlowEditor/generated/Demo Flow.json
:return: 反序列化得到的 GameScriptFlow 实例
"""
path = Path(path)
data = json.loads(path.read_text(encoding="utf-8"))
return cls.load_from_dict(data)
@classmethod
def load_from_dict(cls, data: dict) -> GameScriptFlow:
"""
从字典反序列化为 GameScriptFlow 对象(可含额外键如 node_positions,会被忽略)。
:param data: 含 segments、paths、flags 等键的字典
:return: 反序列化得到的 GameScriptFlow 实例
"""
segments = [cls._segment_from_dict(d) for d in data.get("segments", [])]
paths = [cls._path_from_dict(d) for d in data.get("paths", [])]
flags = [cls._flag_from_dict(d) for d in data.get("flags", [])]
return cls(
name=data.get("name", ""),
id=data.get("id"),
title=data.get("title", ""),
comment=data.get("comment"),
segments=segments,
paths=paths,
flags=flags,
)
@staticmethod
def _segment_from_dict(d: dict) -> StorySegment:
return StorySegment(
name=d["name"],
content=d["content"],
is_ending_segment=d.get("is_ending_segment", True),
id=d.get("id"),
comment=d.get("comment", ""),
paths_segment_ids=d.get("paths_segment_ids", {}),
)
@staticmethod
def _path_from_dict(d: dict) -> SegmentPath:
return SegmentPath(
prev_segment_id=d["prev_segment_id"],
next_segment_id=d["next_segment_id"],
condition_expression=d.get("condition_expression"),
name=d.get("name"),
comment=d.get("comment"),
id=d.get("id"),
)
@staticmethod
def _flag_from_dict(d: dict) -> FlagVariable:
return FlagVariable(
name=d["name"],
flag_type=d["flag_type"],
initial_value=d["initial_value"],
comment=d.get("comment"),
id=d.get("id"),
)
# 序列化为 JSON 并保存到 ScriptFlowEditor/generated 目录下的 .json 文件。
def save_as_json(
self,
filename: str | None = None,
*,
indent: int | None = 2,
ensure_ascii: bool = False,
) -> Path:
"""
将当前对象序列化为 JSON 并保存到 src/ScriptFlowEditor/generated 目录。
:param filename: 文件名(可含 .json 后缀);为 None 时用 name 或 id,不含则自动加 .json。
:param indent: 传给 json.dumps 的缩进,默认 2。
:param ensure_ascii: 是否转义非 ASCII,默认 False 以保留中文。
:return: 保存后的文件路径。
"""
out_dir = self._get_generated_dir()
if not filename:
base = (self.name or self.id or "gamescriptflow").replace("/", "_").replace("\\", "_").strip(". ") or "flow"
filename = f"{base}.json" if not base.lower().endswith(".json") else base
elif not filename.lower().endswith(".json"):
filename = f"{filename}.json"
path = out_dir / filename
path.write_text(self.to_json(indent=indent, ensure_ascii=ensure_ascii), encoding="utf-8")
return path