@zhin.js/plugin-text-adventure 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +92 -0
  2. package/lib/achievements.d.ts +24 -0
  3. package/lib/achievements.d.ts.map +1 -0
  4. package/lib/achievements.js +62 -0
  5. package/lib/achievements.js.map +1 -0
  6. package/lib/adv-command.d.ts +5 -0
  7. package/lib/adv-command.d.ts.map +1 -0
  8. package/lib/adv-command.js +55 -0
  9. package/lib/adv-command.js.map +1 -0
  10. package/lib/commands.d.ts +6 -0
  11. package/lib/commands.d.ts.map +1 -0
  12. package/lib/commands.js +96 -0
  13. package/lib/commands.js.map +1 -0
  14. package/lib/game-flow.d.ts +9 -0
  15. package/lib/game-flow.d.ts.map +1 -0
  16. package/lib/game-flow.js +134 -0
  17. package/lib/game-flow.js.map +1 -0
  18. package/lib/hub-register.d.ts +5 -0
  19. package/lib/hub-register.d.ts.map +1 -0
  20. package/lib/hub-register.js +25 -0
  21. package/lib/hub-register.js.map +1 -0
  22. package/lib/index.d.ts +2 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +37 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/models.d.ts +45 -0
  27. package/lib/models.d.ts.map +1 -0
  28. package/lib/models.js +37 -0
  29. package/lib/models.js.map +1 -0
  30. package/lib/profile-format.d.ts +6 -0
  31. package/lib/profile-format.d.ts.map +1 -0
  32. package/lib/profile-format.js +64 -0
  33. package/lib/profile-format.js.map +1 -0
  34. package/lib/profile-parse.d.ts +4 -0
  35. package/lib/profile-parse.d.ts.map +1 -0
  36. package/lib/profile-parse.js +21 -0
  37. package/lib/profile-parse.js.map +1 -0
  38. package/lib/profile-service.d.ts +17 -0
  39. package/lib/profile-service.d.ts.map +1 -0
  40. package/lib/profile-service.js +124 -0
  41. package/lib/profile-service.js.map +1 -0
  42. package/lib/scene-view.d.ts +4 -0
  43. package/lib/scene-view.d.ts.map +1 -0
  44. package/lib/scene-view.js +32 -0
  45. package/lib/scene-view.js.map +1 -0
  46. package/lib/session-service.d.ts +22 -0
  47. package/lib/session-service.d.ts.map +1 -0
  48. package/lib/session-service.js +100 -0
  49. package/lib/session-service.js.map +1 -0
  50. package/lib/story-catalog.d.ts +13 -0
  51. package/lib/story-catalog.d.ts.map +1 -0
  52. package/lib/story-catalog.js +104 -0
  53. package/lib/story-catalog.js.map +1 -0
  54. package/lib/story-items.d.ts +5 -0
  55. package/lib/story-items.d.ts.map +1 -0
  56. package/lib/story-items.js +23 -0
  57. package/lib/story-items.js.map +1 -0
  58. package/lib/story-path.d.ts +3 -0
  59. package/lib/story-path.d.ts.map +1 -0
  60. package/lib/story-path.js +66 -0
  61. package/lib/story-path.js.map +1 -0
  62. package/lib/story-scenes.d.ts +9 -0
  63. package/lib/story-scenes.d.ts.map +1 -0
  64. package/lib/story-scenes.js +478 -0
  65. package/lib/story-scenes.js.map +1 -0
  66. package/lib/story-transitions.d.ts +3 -0
  67. package/lib/story-transitions.d.ts.map +1 -0
  68. package/lib/story-transitions.js +193 -0
  69. package/lib/story-transitions.js.map +1 -0
  70. package/lib/story-types.d.ts +30 -0
  71. package/lib/story-types.d.ts.map +1 -0
  72. package/lib/story-types.js +3 -0
  73. package/lib/story-types.js.map +1 -0
  74. package/lib/story.d.ts +21 -0
  75. package/lib/story.d.ts.map +1 -0
  76. package/lib/story.js +91 -0
  77. package/lib/story.js.map +1 -0
  78. package/package.json +54 -0
  79. package/plugin.yml +2 -0
  80. package/src/achievements.ts +90 -0
  81. package/src/adv-command.ts +72 -0
  82. package/src/commands.ts +133 -0
  83. package/src/game-flow.ts +196 -0
  84. package/src/hub-register.ts +32 -0
  85. package/src/index.ts +47 -0
  86. package/src/models.ts +84 -0
  87. package/src/profile-format.ts +76 -0
  88. package/src/profile-parse.ts +19 -0
  89. package/src/profile-service.ts +151 -0
  90. package/src/scene-view.ts +46 -0
  91. package/src/session-service.ts +113 -0
  92. package/src/story-catalog.ts +110 -0
  93. package/src/story-items.ts +24 -0
  94. package/src/story-path.ts +75 -0
  95. package/src/story-scenes.ts +508 -0
  96. package/src/story-transitions.ts +198 -0
  97. package/src/story-types.ts +33 -0
  98. package/src/story.ts +104 -0
@@ -0,0 +1,508 @@
1
+ import type { GameState, Scene } from './story-types.js';
2
+
3
+ const T = (lines: string[]) => lines.join('\n');
4
+
5
+ /** 非终局场景 28 个 + 终局 12 个 */
6
+ export const SCENES: Record<string, Scene> = {
7
+ start: {
8
+ id: 'start',
9
+ text: (s) => {
10
+ const lines = [
11
+ '🌫️ **秘境入口**',
12
+ '',
13
+ '浓雾散去,你站在古老石门前。左侧是坍塌廊柱,右侧藤蔓小径,远处石塔刺破雾霭。',
14
+ ];
15
+ if (s.inventory.includes('torch')) lines.push('', '🔥 火把照亮门缝间的幽蓝微光。');
16
+ if (s.flags.spirit_met) lines.push('', '💧 泉灵低语:「庭院与星塔皆藏路。」');
17
+ if (s.flags.saw_map) lines.push('', '🗺️ 你记得塔顶俯瞰的全貌:泉、庭、墓、门。');
18
+ return lines.join('\n');
19
+ },
20
+ choices: [
21
+ { id: 'push_door', label: '🚪 推门而入', style: 'primary' },
22
+ { id: 'search_rubble', label: '🔍 翻找碎石' },
23
+ { id: 'climb_tower', label: '🗼 攀登石塔' },
24
+ { id: 'vine_path', label: '🌿 拨开藤蔓', requires: (s) => s.flags.spirit_met },
25
+ { id: 'leave', label: '🏃 转身离开', style: 'danger' },
26
+ ],
27
+ },
28
+
29
+ rubble: {
30
+ id: 'rubble',
31
+ text: T([
32
+ '你在坍塌廊柱间翻找,指尖触到冰冷金属。',
33
+ '一把**火把**躺在尘土里;石缝深处还有生锈短剑。',
34
+ ]),
35
+ choices: [
36
+ { id: 'take_torch', label: '🔥 拾取火把', requires: (s) => !s.inventory.includes('torch') },
37
+ { id: 'take_blade', label: '⚔️ 拾取短剑', requires: (s) => !s.inventory.includes('rusted_blade') },
38
+ { id: 'back_entrance', label: '↩️ 返回入口' },
39
+ ],
40
+ },
41
+
42
+ watchtower: {
43
+ id: 'watchtower',
44
+ text: (s) => T([
45
+ '🗼 **瞭望石塔**',
46
+ '',
47
+ s.flags.saw_map
48
+ ? '塔顶风声呼啸,你已记下遗迹布局。'
49
+ : '螺旋阶梯通向塔顶,视野开阔处或能看清秘境结构。',
50
+ '塔身裂缝中卡着一页星图残角。',
51
+ ]),
52
+ choices: [
53
+ { id: 'survey_map', label: '🗺️ 登高测绘', requires: (s) => !s.flags.saw_map },
54
+ { id: 'take_chart_fragment', label: '📄 取星图残页', requires: (s) => !s.inventory.includes('star_chart') },
55
+ { id: 'enter_observatory', label: '🔭 进入星象台', requires: (s) => s.flags.saw_map },
56
+ { id: 'descend_tower', label: '↩️ 下塔' },
57
+ ],
58
+ },
59
+
60
+ vine_path: {
61
+ id: 'vine_path',
62
+ text: T(['🌿 **秘径**', '', '青苔石阶向下,药香与湿土气息交织。']),
63
+ choices: [
64
+ { id: 'enter_garden', label: '🏡 进入庭院' },
65
+ { id: 'back_entrance', label: '↩️ 返回入口' },
66
+ ],
67
+ },
68
+
69
+ corridor: {
70
+ id: 'corridor',
71
+ text: (s) => {
72
+ const lit = s.inventory.includes('torch');
73
+ const lines = [
74
+ lit ? '🕯️ 火把照亮狭长走廊。' : '🌑 走廊漆黑,你贴墙摸索。',
75
+ '左侧蓝光(泉)、右侧石阶(墓)、前方积水(淹)、壁画需火光细读。',
76
+ ];
77
+ if (s.flags.knows_sequence) lines.push('', '📜 壁画铭文:光 → 影 → 门。');
78
+ return lines.join('\n');
79
+ },
80
+ choices: [
81
+ { id: 'go_blue', label: '💧 走向蓝光' },
82
+ { id: 'go_stairs', label: '⬇️ 走下石阶' },
83
+ { id: 'go_flooded', label: '🌊 踏入积水厅' },
84
+ { id: 'study_mural', label: '🖼️ 研读壁画', requires: (s) => s.inventory.includes('torch') },
85
+ { id: 'enter_bell_tower', label: '🔔 进入钟楼', requires: (s) => s.flags.spirit_met },
86
+ { id: 'go_back', label: '↩️ 退回入口' },
87
+ ],
88
+ },
89
+
90
+ mural_room: {
91
+ id: 'mural_room',
92
+ text: T([
93
+ '🖼️ **铭文壁画**',
94
+ '',
95
+ '三联画:光、影、门。下角刻序:**光 → 影 → 门**。',
96
+ '壁画后有一条狭窄画廊,低语声如风穿隙。',
97
+ ]),
98
+ choices: [
99
+ { id: 'memorize', label: '📝 记下顺序' },
100
+ { id: 'enter_whisper', label: '👂 进入低语画廊', requires: (s) => s.flags.knows_sequence },
101
+ { id: 'back_corridor', label: '↩️ 返回走廊' },
102
+ ],
103
+ },
104
+
105
+ whisper_gallery: {
106
+ id: 'whisper_gallery',
107
+ text: T([
108
+ '👂 **低语画廊**',
109
+ '',
110
+ '亿万精灵名讳在墙间回响。你听见:「星坠之日,门为封印,非为宝藏。」',
111
+ '「守忆者需集月华、星图、泉心三证。」',
112
+ ]),
113
+ choices: [
114
+ { id: 'listen_deep', label: '🧘 静心聆听', requires: (s) => !s.flags.whisper_lore },
115
+ { id: 'back_mural', label: '↩️ 返回壁画室' },
116
+ ],
117
+ },
118
+
119
+ spring: {
120
+ id: 'spring',
121
+ text: (s) => T([
122
+ '💧 **精灵泉**',
123
+ '',
124
+ s.flags.spirit_met ? '泉灵光影朝你颔首。' : '水光凝聚为精灵形貌,注视着你。',
125
+ !s.flags.healed ? '泉水可治愈一次。' : '泉水已无先前效力。',
126
+ ]),
127
+ choices: [
128
+ { id: 'drink', label: '🍶 饮用泉水', requires: (s) => !s.flags.healed },
129
+ { id: 'take_gem', label: '💎 取走宝石', requires: (s) => !s.inventory.includes('gem') },
130
+ { id: 'talk_spirit', label: '💬 与泉灵交谈', requires: (s) => !s.flags.spirit_met },
131
+ { id: 'offer_gem', label: '🙏 献宝石换护符', requires: (s) => s.inventory.includes('gem') && !s.inventory.includes('amulet') },
132
+ { id: 'leave_spring', label: '↩️ 离开' },
133
+ ],
134
+ },
135
+
136
+ flooded_hall: {
137
+ id: 'flooded_hall',
138
+ text: (s) => T([
139
+ '🌊 **积水前厅**',
140
+ '',
141
+ '及膝冷水没过石板,远处有坍塌市集拱门。',
142
+ s.inventory.includes('elf_feather') ? '银羽护体,寒气难侵。' : '寒意刺骨,久留恐失温。',
143
+ ]),
144
+ choices: [
145
+ { id: 'wade_market', label: '🏚️ 前往废墟市集' },
146
+ { id: 'use_feather_wade', label: '🪶 银羽护体涉水', requires: (s) => s.inventory.includes('elf_feather') },
147
+ { id: 'drink_elixir_warm', label: '🍶 饮灵药暖身', requires: (s) => s.inventory.includes('elixir') },
148
+ { id: 'back_corridor', label: '↩️ 返回走廊' },
149
+ ],
150
+ },
151
+
152
+ market_ruins: {
153
+ id: 'market_ruins',
154
+ text: T([
155
+ '🏚️ **废墟市集**',
156
+ '',
157
+ '幽灵摊主陈列面具与古币。「一物换一物,或银币亦可。」',
158
+ ]),
159
+ choices: [
160
+ { id: 'buy_mask_coin', label: '🎭 银币换面具', requires: (s) => s.inventory.includes('silver_coin') && !s.inventory.includes('masquerade_mask') },
161
+ { id: 'trade_gem_coin', label: '💎 宝石换银币', requires: (s) => s.inventory.includes('gem') },
162
+ { id: 'go_bridge', label: '🌉 前往冥河石桥' },
163
+ { id: 'leave_market', label: '↩️ 离开市集' },
164
+ ],
165
+ },
166
+
167
+ garden: {
168
+ id: 'garden',
169
+ text: (s) => T([
170
+ '🏡 **遗忘庭院**',
171
+ '',
172
+ '银叶古树、紧锁铁门、侧翼温室玻璃碎裂。',
173
+ s.inventory.includes('herb') ? '花坛已采过药草。' : '花坛闪烁荧光草药。',
174
+ ]),
175
+ choices: [
176
+ { id: 'pick_herb', label: '🌿 采集草药', requires: (s) => !s.inventory.includes('herb') },
177
+ { id: 'rest_tree', label: '🌳 树下休息', requires: (s) => !s.flags.rested },
178
+ { id: 'enter_greenhouse', label: '🪴 进入温室' },
179
+ { id: 'open_iron_door', label: '🔑 打开铁门', requires: (s) => s.inventory.includes('key') },
180
+ { id: 'stay_forever', label: '🏠 定居庭院', requires: (s) => s.flags.rested && s.flags.spirit_met },
181
+ { id: 'back_corridor', label: '↩️ 返回走廊' },
182
+ ],
183
+ },
184
+
185
+ greenhouse: {
186
+ id: 'greenhouse',
187
+ text: T([
188
+ '🪴 **破碎温室**',
189
+ '',
190
+ '枯藤缠绕蒸馏器,可萃取草药为灵药。',
191
+ ]),
192
+ choices: [
193
+ { id: 'brew_elixir', label: '⚗️ 萃取灵药', requires: (s) => s.inventory.includes('herb') && !s.inventory.includes('elixir') },
194
+ { id: 'back_garden', label: '↩️ 返回庭院' },
195
+ ],
196
+ },
197
+
198
+ library: {
199
+ id: 'library',
200
+ text: (s) => T([
201
+ '📚 **尘封书库**',
202
+ '',
203
+ '残卷、墙钥、暗门后传来锻炉热气。',
204
+ s.flags.read_archive ? '你已读过秘档。' : '古籍区尚有未读档案。',
205
+ ]),
206
+ choices: [
207
+ { id: 'take_scroll', label: '📜 取卷轴', requires: (s) => !s.inventory.includes('scroll') },
208
+ { id: 'take_key', label: '🔑 取钥匙', requires: (s) => !s.inventory.includes('key') },
209
+ { id: 'read_archive', label: '📖 研读秘档', requires: (s) => !s.flags.read_archive },
210
+ { id: 'enter_forge', label: '🔥 进入锻炉间', requires: (s) => s.inventory.includes('scroll') },
211
+ { id: 'back_garden', label: '↩️ 返回庭院' },
212
+ ],
213
+ },
214
+
215
+ ember_forge: {
216
+ id: 'ember_forge',
217
+ text: (s) => T([
218
+ '🔥 **余烬锻炉**',
219
+ '',
220
+ '精灵余火仍燃。可淬炼短剑,或升华护符。',
221
+ s.flags.forged_blade ? '短剑已淬炼。' : '短剑可淬炼。',
222
+ ]),
223
+ choices: [
224
+ { id: 'forge_blade', label: '⚔️ 淬炼短剑', requires: (s) => s.inventory.includes('rusted_blade') && !s.flags.forged_blade },
225
+ { id: 'upgrade_amulet', label: '✨ 升华护符', requires: (s) => s.inventory.includes('amulet') && s.inventory.includes('moon_shard') && !s.flags.amulet_upgraded },
226
+ { id: 'back_library', label: '↩️ 返回书库' },
227
+ ],
228
+ },
229
+
230
+ bell_tower: {
231
+ id: 'bell_tower',
232
+ text: T([
233
+ '🔔 **回音钟楼**',
234
+ '',
235
+ '悬空铜钟布满符纹。钟舌缺失,旁有铸台。',
236
+ ]),
237
+ choices: [
238
+ { id: 'ring_bell', label: '🔔 敲响铜钟', requires: (s) => !s.inventory.includes('spirit_bell') },
239
+ { id: 'back_corridor', label: '↩️ 返回走廊' },
240
+ ],
241
+ },
242
+
243
+ depths: {
244
+ id: 'depths',
245
+ text: T([
246
+ '⬇️ **地下墓穴**',
247
+ '',
248
+ '石棺半开,**守墓幽影**凝形扑来!',
249
+ ]),
250
+ choices: [
251
+ { id: 'fight', label: '⚔️ 正面迎战', style: 'danger' },
252
+ { id: 'fight_blade', label: '🗡️ 挥剑迎击', requires: (s) => s.flags.forged_blade },
253
+ { id: 'use_torch', label: '🔥 火把驱散', requires: (s) => s.inventory.includes('torch') },
254
+ { id: 'use_herb', label: '🌿 草药麻痹', requires: (s) => s.inventory.includes('herb') },
255
+ { id: 'flee', label: '🏃 逃跑' },
256
+ ],
257
+ },
258
+
259
+ crypt_entry: {
260
+ id: 'crypt_entry',
261
+ text: T([
262
+ '⚰️ **墓道岔口**',
263
+ '',
264
+ '左:封印密室|右:骸骨甬道|下:蛛巢暗道',
265
+ ]),
266
+ choices: [
267
+ { id: 'go_vault', label: '🔐 封印密室' },
268
+ { id: 'go_catacombs', label: '💀 骸骨甬道' },
269
+ { id: 'go_spider', label: '🕷️ 蛛巢暗道' },
270
+ { id: 'go_sealed', label: '🚪 回音巨门', requires: (s) => s.inventory.includes('spirit_bell') && s.inventory.includes('compass') },
271
+ { id: 'back_corridor', label: '↩️ 返回走廊' },
272
+ ],
273
+ },
274
+
275
+ spider_nest: {
276
+ id: 'spider_nest',
277
+ text: T([
278
+ '🕷️ **蛛巢暗道**',
279
+ '',
280
+ '巨网横亘,蛛眼如灯笼。后方似通暗影迷宫。',
281
+ ]),
282
+ choices: [
283
+ { id: 'cut_web_blade', label: '🗡️ 斩网前行', requires: (s) => s.flags.forged_blade },
284
+ { id: 'burn_web', label: '🔥 火烧蛛网', requires: (s) => s.inventory.includes('torch') },
285
+ { id: 'flee_spider', label: '🏃 撤退', style: 'danger' },
286
+ ],
287
+ },
288
+
289
+ shadow_maze: {
290
+ id: 'shadow_maze',
291
+ text: T([
292
+ '🌑 **暗影迷宫**',
293
+ '',
294
+ '墙壁流动如墨,路径每秒变幻。',
295
+ ]),
296
+ choices: [
297
+ { id: 'navigate_compass', label: '🧭 罗盘定向', requires: (s) => s.inventory.includes('compass') },
298
+ { id: 'wander_maze', label: '🚶 随意乱走', style: 'danger' },
299
+ { id: 'back_spider', label: '↩️ 返回蛛巢' },
300
+ ],
301
+ },
302
+
303
+ catacombs: {
304
+ id: 'catacombs',
305
+ text: T([
306
+ '💀 **骸骨甬道**',
307
+ '',
308
+ '压力机关铭刻:诵卷轴真言。侧室铁链锁着幽魂。',
309
+ '骸骨手中铜钥闪光。',
310
+ ]),
311
+ choices: [
312
+ { id: 'read_scroll', label: '📜 展开卷轴', requires: (s) => s.inventory.includes('scroll') },
313
+ { id: 'dash_trap', label: '🏃 硬闯机关', style: 'danger' },
314
+ { id: 'enter_prison', label: '⛓️ 进入囚室' },
315
+ { id: 'take_key_skeleton', label: '🔑 取铜钥', requires: (s) => !s.inventory.includes('key') },
316
+ { id: 'back_crypt', label: '↩️ 返回岔口' },
317
+ ],
318
+ },
319
+
320
+ prison_cell: {
321
+ id: 'prison_cell',
322
+ text: T([
323
+ '⛓️ **幽囚石室**',
324
+ '',
325
+ '幽魂哀求:「银币可买通行,羽毛可渡冥河。」',
326
+ '墙角埋着古银币。',
327
+ ]),
328
+ choices: [
329
+ { id: 'take_coin', label: '🪙 取古银币', requires: (s) => !s.inventory.includes('silver_coin') },
330
+ { id: 'free_ghost', label: '🕊️ 释放幽魂', requires: (s) => s.inventory.includes('elf_feather') },
331
+ { id: 'back_catacombs', label: '↩️ 返回甬道' },
332
+ ],
333
+ },
334
+
335
+ ossuary: {
336
+ id: 'ossuary',
337
+ text: T([
338
+ '🪦 **骨灰龛室**',
339
+ '',
340
+ '悬浮护符。幽魂问:「精灵遗迹为何陨落?」',
341
+ ]),
342
+ choices: [
343
+ { id: 'answer_greed', label: '「贪婪与战争」' },
344
+ { id: 'answer_flood', label: '「大洪水」' },
345
+ { id: 'answer_star', label: '「星辰坠落」', style: 'primary' },
346
+ { id: 'back_catacombs', label: '↩️ 返回甬道' },
347
+ ],
348
+ },
349
+
350
+ ghost_bridge: {
351
+ id: 'ghost_bridge',
352
+ text: T([
353
+ '🌉 **冥河石桥**',
354
+ '',
355
+ '无栏石桥横跨黑水。守桥者伸手要通行费。',
356
+ ]),
357
+ choices: [
358
+ { id: 'pay_coin', label: '🪙 付银币过桥', requires: (s) => s.inventory.includes('silver_coin') },
359
+ { id: 'cross_feather', label: '🪶 银羽渡桥', requires: (s) => s.inventory.includes('elf_feather') },
360
+ { id: 'back_market', label: '↩️ 返回市集' },
361
+ ],
362
+ },
363
+
364
+ crystal_cave: {
365
+ id: 'crystal_cave',
366
+ text: T([
367
+ '💎 **晶洞**',
368
+ '',
369
+ '月华凝于石笋。洞顶飘落银羽。',
370
+ ]),
371
+ choices: [
372
+ { id: 'take_moon_shard', label: '🌙 取月华碎片', requires: (s) => !s.inventory.includes('moon_shard') },
373
+ { id: 'take_feather', label: '🪶 拾银羽', requires: (s) => !s.inventory.includes('elf_feather') },
374
+ { id: 'enter_moon_pool', label: '💧 前往月池' },
375
+ { id: 'back_bridge', label: '↩️ 返回石桥' },
376
+ ],
377
+ },
378
+
379
+ moon_pool: {
380
+ id: 'moon_pool',
381
+ text: (s) => T([
382
+ '🌙 **月影池**',
383
+ '',
384
+ '池水映月。星图残页可与之共鸣。',
385
+ s.flags.moon_blessed ? '月华已赐福。' : '池底似有水下沉殿。',
386
+ ]),
387
+ choices: [
388
+ { id: 'align_chart', label: '📄 星图共鸣', requires: (s) => s.inventory.includes('star_chart') && !s.flags.moon_blessed },
389
+ { id: 'dive_shrine', label: '🤿 潜入沉殿', requires: (s) => s.flags.moon_blessed },
390
+ { id: 'back_crystal', label: '↩️ 返回晶洞' },
391
+ ],
392
+ },
393
+
394
+ sunken_shrine: {
395
+ id: 'sunken_shrine',
396
+ text: T([
397
+ '🛕 **水下沉殿**',
398
+ '',
399
+ '三证石台:月华、星图、泉心印记。缺一则门不开。',
400
+ ]),
401
+ choices: [
402
+ { id: 'ritual_star_keeper', label: '✨ 完成星守仪式', requires: (s) => s.inventory.includes('moon_shard') && s.inventory.includes('star_chart') && s.flags.spirit_met },
403
+ { id: 'back_moon_pool', label: '↩️ 浮上水面' },
404
+ ],
405
+ },
406
+
407
+ observatory: {
408
+ id: 'observatory',
409
+ text: T([
410
+ '🔭 **星象台**',
411
+ '',
412
+ '铜制浑天仪缺损。可校准罗盘。',
413
+ ]),
414
+ choices: [
415
+ { id: 'craft_compass', label: '🧭 校准罗盘', requires: (s) => !s.inventory.includes('compass') },
416
+ { id: 'back_tower', label: '↩️ 下塔' },
417
+ ],
418
+ },
419
+
420
+ sealed_gates: {
421
+ id: 'sealed_gates',
422
+ text: T([
423
+ '🚪 **回音巨门**',
424
+ '',
425
+ '铃、罗盘、钥三缺一时铭文闪烁。门后是封印内庭捷径。',
426
+ ]),
427
+ choices: [
428
+ { id: 'open_gates', label: '🔓 开启巨门', requires: (s) => s.inventory.includes('spirit_bell') && s.inventory.includes('compass') && s.inventory.includes('key') },
429
+ { id: 'back_crypt', label: '↩️ 返回岔口' },
430
+ ],
431
+ },
432
+
433
+ vault: {
434
+ id: 'vault',
435
+ text: (s) => T([
436
+ '🔐 **封印密室**',
437
+ '',
438
+ '三雕像:光、影、门。',
439
+ s.flags.knows_sequence ? '顺序:光 → 影 → 门。' : '顺序未知。',
440
+ ]),
441
+ choices: [
442
+ { id: 'seq_light_shadow_door', label: '光→影→门', style: 'primary' },
443
+ { id: 'seq_door_light_shadow', label: '门→光→影' },
444
+ { id: 'seq_shadow_door_light', label: '影→门→光' },
445
+ { id: 'back_crypt', label: '↩️ 返回岔口' },
446
+ ],
447
+ },
448
+
449
+ vault_open: {
450
+ id: 'vault_open',
451
+ text: (s) => T([
452
+ '✨ **封印已解**',
453
+ '',
454
+ s.flags.amulet_upgraded ? '升华护符引路,王座厅近在咫尺。' : '螺旋阶梯通向王座厅。',
455
+ ]),
456
+ choices: [
457
+ { id: 'descend_throne', label: '⬇️ 王座厅' },
458
+ { id: 'take_treasure_now', label: '💰 取宝离开' },
459
+ { id: 'back_crypt', label: '↩️ 返回岔口' },
460
+ ],
461
+ },
462
+
463
+ throne_room: {
464
+ id: 'throne_room',
465
+ text: (s) => {
466
+ const ready = s.flags.whisper_lore && s.flags.amulet_upgraded && s.flags.spirit_met;
467
+ return T([
468
+ '👑 **精灵王座厅**',
469
+ '',
470
+ ready ? '三证齐聚,王座虚影恭迎守忆者。' : '王座冷光闪烁,贸然落座恐遭反噬。',
471
+ ]);
472
+ },
473
+ choices: [
474
+ {
475
+ id: 'ascend',
476
+ label: '✨ 星辉归位',
477
+ style: 'primary',
478
+ requires: (s) => s.flags.whisper_lore && s.flags.amulet_upgraded && s.flags.spirit_met && s.inventory.includes('scroll'),
479
+ },
480
+ { id: 'ascend_mask', label: '🎭 面具加冕', requires: (s) => s.inventory.includes('masquerade_mask') },
481
+ { id: 'claim_crown', label: '👑 坐上王座', style: 'danger' },
482
+ { id: 'retreat_throne', label: '🏃 取宝离开' },
483
+ ],
484
+ },
485
+
486
+ // —— 终局 ——
487
+ treasure: { id: 'treasure', terminal: true, text: T(['🏆 **遗迹宝藏**', '', '琥珀星光入袋,你成为传说。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
488
+ ascension: { id: 'ascension', terminal: true, text: T(['🌟 **星辉归位**', '', '你成为守忆者,历史因你而不朽。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
489
+ wisdom: { id: 'wisdom', terminal: true, text: T(['📜 **智者归途**', '', '你携记忆离去,无宝亦足。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
490
+ curse: { id: 'curse', terminal: true, text: T(['☠️ **贪婪代价**', '', '王座吞噬了你。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
491
+ escape: { id: 'escape', terminal: true, text: T(['🌅 **生还**', '', '遗迹坍塌,你活着出来。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
492
+ defeat: { id: 'defeat', terminal: true, text: T(['💀 **陨落**', '', '秘境再添无名骸骨。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
493
+ coward: { id: 'coward', terminal: true, text: T(['🏠 **怯懦归途**', '', '你从未入门。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
494
+ hermit: { id: 'hermit', terminal: true, text: T(['🌳 **庭院隐者**', '', '你在古树下定居,与遗迹共生。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
495
+ redemption: { id: 'redemption', terminal: true, text: T(['🕊️ **幽魂解脱**', '', '你渡魂过桥,获冥府祝福。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
496
+ drowned: { id: 'drowned', terminal: true, text: T(['🌊 **寒水沉眠**', '', '积水厅吞没意识。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
497
+ spider: { id: 'spider', terminal: true, text: T(['🕷️ **茧中亡魂**', '', '巨蛛将你制成茧。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
498
+ labyrinth: { id: 'labyrinth', terminal: true, text: T(['🌑 **迷宫迷失**', '', '暗影吞没方向感。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
499
+ star_keeper: { id: 'star_keeper', terminal: true, text: T(['🌙 **星守誓约**', '', '你镇守月池,成为精灵最后的灯塔。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
500
+ mask_king: { id: 'mask_king', terminal: true, text: T(['🎭 **面具之王**', '', '面具与王座融合,你统治空城。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
501
+ merchant: { id: 'merchant', terminal: true, text: T(['💰 **行商结局**', '', '你满载银币离开,遗迹不过集市。']), choices: [{ id: 'restart', label: '🔄 再玩一次', style: 'primary' }] },
502
+ };
503
+
504
+ export function countScenes(): { total: number; playable: number; terminals: number } {
505
+ const all = Object.values(SCENES);
506
+ const terminals = all.filter((s) => s.terminal).length;
507
+ return { total: all.length, playable: all.length - terminals, terminals };
508
+ }