@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,198 @@
1
+ import type { ChoiceResult, GameState } from './story-types.js';
2
+
3
+ export function resolveTransition(state: GameState, choiceId: string): ChoiceResult | null {
4
+ const { sceneId } = state;
5
+
6
+ if (choiceId === 'restart') {
7
+ return { nextSceneId: 'start', hpDelta: 100 - state.hp, endingId: '' };
8
+ }
9
+
10
+ if (sceneId === 'flooded_hall' && choiceId === 'wade_market') {
11
+ const hp = state.hp - 25;
12
+ if (hp <= 0) return { nextSceneId: 'drowned', hpDelta: -25, endingId: 'drowned' };
13
+ return { nextSceneId: 'market_ruins', hpDelta: -25 };
14
+ }
15
+
16
+ const hpFail = (next: string, delta: number): ChoiceResult => ({
17
+ nextSceneId: state.hp + delta <= 0 ? 'defeat' : next,
18
+ hpDelta: delta,
19
+ endingId: state.hp + delta <= 0 ? 'defeat' : '',
20
+ });
21
+
22
+ const map: Record<string, Record<string, ChoiceResult>> = {
23
+ start: {
24
+ push_door: { nextSceneId: 'corridor' },
25
+ search_rubble: { nextSceneId: 'rubble' },
26
+ climb_tower: { nextSceneId: 'watchtower' },
27
+ vine_path: { nextSceneId: 'vine_path' },
28
+ leave: { nextSceneId: 'coward', endingId: 'coward' },
29
+ },
30
+ rubble: {
31
+ take_torch: { nextSceneId: 'start', addItem: 'torch' },
32
+ take_blade: { nextSceneId: 'start', addItem: 'rusted_blade' },
33
+ back_entrance: { nextSceneId: 'start' },
34
+ },
35
+ watchtower: {
36
+ survey_map: { nextSceneId: 'watchtower', setFlag: 'saw_map' },
37
+ take_chart_fragment: { nextSceneId: 'watchtower', addItem: 'star_chart' },
38
+ enter_observatory: { nextSceneId: 'observatory' },
39
+ descend_tower: { nextSceneId: 'start' },
40
+ },
41
+ vine_path: {
42
+ enter_garden: { nextSceneId: 'garden' },
43
+ back_entrance: { nextSceneId: 'start' },
44
+ },
45
+ corridor: {
46
+ go_blue: { nextSceneId: 'spring' },
47
+ go_stairs: { nextSceneId: 'depths' },
48
+ go_flooded: { nextSceneId: 'flooded_hall' },
49
+ study_mural: { nextSceneId: 'mural_room' },
50
+ enter_bell_tower: { nextSceneId: 'bell_tower' },
51
+ go_back: { nextSceneId: 'start' },
52
+ },
53
+ mural_room: {
54
+ memorize: { nextSceneId: 'mural_room', setFlag: 'knows_sequence' },
55
+ enter_whisper: { nextSceneId: 'whisper_gallery' },
56
+ back_corridor: { nextSceneId: 'corridor' },
57
+ },
58
+ whisper_gallery: {
59
+ listen_deep: { nextSceneId: 'whisper_gallery', setFlag: 'whisper_lore' },
60
+ back_mural: { nextSceneId: 'mural_room' },
61
+ },
62
+ spring: {
63
+ drink: { nextSceneId: 'spring', hpDelta: 30, setFlag: 'healed' },
64
+ take_gem: { nextSceneId: 'spring', addItem: 'gem' },
65
+ talk_spirit: { nextSceneId: 'spring', setFlag: 'spirit_met' },
66
+ offer_gem: { nextSceneId: 'spring', removeItem: 'gem', addItem: 'amulet', setFlag: 'spirit_blessed' },
67
+ leave_spring: { nextSceneId: 'corridor' },
68
+ },
69
+ flooded_hall: {
70
+ use_feather_wade: { nextSceneId: 'market_ruins', removeItem: 'elf_feather' },
71
+ drink_elixir_warm: { nextSceneId: 'market_ruins', removeItem: 'elixir' },
72
+ back_corridor: { nextSceneId: 'corridor' },
73
+ },
74
+ market_ruins: {
75
+ buy_mask_coin: { nextSceneId: 'market_ruins', removeItem: 'silver_coin', addItem: 'masquerade_mask' },
76
+ trade_gem_coin: { nextSceneId: 'market_ruins', removeItem: 'gem', addItem: 'silver_coin' },
77
+ go_bridge: { nextSceneId: 'ghost_bridge' },
78
+ leave_market: { nextSceneId: 'flooded_hall' },
79
+ },
80
+ garden: {
81
+ pick_herb: { nextSceneId: 'garden', addItem: 'herb' },
82
+ rest_tree: { nextSceneId: 'garden', hpDelta: 20, setFlag: 'rested' },
83
+ enter_greenhouse: { nextSceneId: 'greenhouse' },
84
+ open_iron_door: { nextSceneId: 'library' },
85
+ stay_forever: { nextSceneId: 'hermit', endingId: 'hermit' },
86
+ back_corridor: { nextSceneId: 'corridor' },
87
+ },
88
+ greenhouse: {
89
+ brew_elixir: { nextSceneId: 'greenhouse', removeItem: 'herb', addItem: 'elixir' },
90
+ back_garden: { nextSceneId: 'garden' },
91
+ },
92
+ library: {
93
+ take_scroll: { nextSceneId: 'library', addItem: 'scroll' },
94
+ take_key: { nextSceneId: 'library', addItem: 'key' },
95
+ read_archive: { nextSceneId: 'library', setFlag: 'read_archive' },
96
+ enter_forge: { nextSceneId: 'ember_forge' },
97
+ back_garden: { nextSceneId: 'garden' },
98
+ },
99
+ ember_forge: {
100
+ forge_blade: { nextSceneId: 'ember_forge', setFlag: 'forged_blade' },
101
+ upgrade_amulet: { nextSceneId: 'ember_forge', removeItem: 'moon_shard', setFlag: 'amulet_upgraded' },
102
+ back_library: { nextSceneId: 'library' },
103
+ },
104
+ bell_tower: {
105
+ ring_bell: { nextSceneId: 'bell_tower', addItem: 'spirit_bell' },
106
+ back_corridor: { nextSceneId: 'corridor' },
107
+ },
108
+ depths: {
109
+ fight: hpFail('crypt_entry', -50),
110
+ fight_blade: { nextSceneId: 'crypt_entry', hpDelta: -20, setFlag: 'shadow_banished' },
111
+ use_torch: { nextSceneId: 'crypt_entry', setFlag: 'shadow_banished' },
112
+ use_herb: { nextSceneId: 'crypt_entry', removeItem: 'herb', setFlag: 'shadow_banished' },
113
+ flee: { nextSceneId: 'escape', endingId: 'escape' },
114
+ },
115
+ crypt_entry: {
116
+ go_vault: { nextSceneId: 'vault' },
117
+ go_catacombs: { nextSceneId: 'catacombs' },
118
+ go_spider: { nextSceneId: 'spider_nest' },
119
+ go_sealed: { nextSceneId: 'sealed_gates' },
120
+ back_corridor: { nextSceneId: 'corridor' },
121
+ },
122
+ spider_nest: {
123
+ cut_web_blade: { nextSceneId: 'shadow_maze' },
124
+ burn_web: { nextSceneId: 'shadow_maze' },
125
+ flee_spider: { nextSceneId: 'spider', endingId: 'spider' },
126
+ },
127
+ shadow_maze: {
128
+ navigate_compass: { nextSceneId: 'crystal_cave' },
129
+ wander_maze: { nextSceneId: 'labyrinth', endingId: 'labyrinth' },
130
+ back_spider: { nextSceneId: 'spider_nest' },
131
+ },
132
+ catacombs: {
133
+ read_scroll: { nextSceneId: 'ossuary' },
134
+ dash_trap: hpFail('ossuary', -35),
135
+ enter_prison: { nextSceneId: 'prison_cell' },
136
+ take_key_skeleton: { nextSceneId: 'catacombs', addItem: 'key' },
137
+ back_crypt: { nextSceneId: 'crypt_entry' },
138
+ },
139
+ prison_cell: {
140
+ take_coin: { nextSceneId: 'prison_cell', addItem: 'silver_coin' },
141
+ free_ghost: { nextSceneId: 'redemption', endingId: 'redemption' },
142
+ back_catacombs: { nextSceneId: 'catacombs' },
143
+ },
144
+ ossuary: {
145
+ answer_greed: { nextSceneId: 'ossuary', hpDelta: -25 },
146
+ answer_flood: { nextSceneId: 'wisdom', endingId: 'wisdom' },
147
+ answer_star: { nextSceneId: 'ossuary', addItem: 'amulet' },
148
+ back_catacombs: { nextSceneId: 'catacombs' },
149
+ },
150
+ ghost_bridge: {
151
+ pay_coin: { nextSceneId: 'crystal_cave', removeItem: 'silver_coin' },
152
+ cross_feather: { nextSceneId: 'crystal_cave', removeItem: 'elf_feather' },
153
+ back_market: { nextSceneId: 'market_ruins' },
154
+ },
155
+ crystal_cave: {
156
+ take_moon_shard: { nextSceneId: 'crystal_cave', addItem: 'moon_shard' },
157
+ take_feather: { nextSceneId: 'crystal_cave', addItem: 'elf_feather' },
158
+ enter_moon_pool: { nextSceneId: 'moon_pool' },
159
+ back_bridge: { nextSceneId: 'ghost_bridge' },
160
+ },
161
+ moon_pool: {
162
+ align_chart: { nextSceneId: 'moon_pool', setFlag: 'moon_blessed' },
163
+ dive_shrine: { nextSceneId: 'sunken_shrine' },
164
+ back_crystal: { nextSceneId: 'crystal_cave' },
165
+ },
166
+ sunken_shrine: {
167
+ ritual_star_keeper: { nextSceneId: 'star_keeper', endingId: 'star_keeper' },
168
+ back_moon_pool: { nextSceneId: 'moon_pool' },
169
+ },
170
+ observatory: {
171
+ craft_compass: { nextSceneId: 'observatory', addItem: 'compass' },
172
+ back_tower: { nextSceneId: 'watchtower' },
173
+ },
174
+ sealed_gates: {
175
+ open_gates: { nextSceneId: 'vault' },
176
+ back_crypt: { nextSceneId: 'crypt_entry' },
177
+ },
178
+ vault: {
179
+ seq_light_shadow_door: { nextSceneId: 'vault_open' },
180
+ seq_door_light_shadow: { nextSceneId: 'wisdom', endingId: 'wisdom' },
181
+ seq_shadow_door_light: { nextSceneId: 'defeat', hpDelta: -100, endingId: 'defeat' },
182
+ back_crypt: { nextSceneId: 'crypt_entry' },
183
+ },
184
+ vault_open: {
185
+ descend_throne: { nextSceneId: 'throne_room' },
186
+ take_treasure_now: { nextSceneId: 'treasure', endingId: 'treasure' },
187
+ back_crypt: { nextSceneId: 'crypt_entry' },
188
+ },
189
+ throne_room: {
190
+ ascend: { nextSceneId: 'ascension', endingId: 'ascension' },
191
+ ascend_mask: { nextSceneId: 'mask_king', endingId: 'mask_king' },
192
+ claim_crown: { nextSceneId: 'curse', endingId: 'curse' },
193
+ retreat_throne: { nextSceneId: 'treasure', endingId: 'treasure' },
194
+ },
195
+ };
196
+
197
+ return map[sceneId]?.[choiceId] ?? null;
198
+ }
@@ -0,0 +1,33 @@
1
+ /** 游戏 payload 前缀 */
2
+ export const ADV_PREFIX = 'adv';
3
+
4
+ export interface SceneChoice {
5
+ id: string;
6
+ label: string;
7
+ style?: 'primary' | 'danger' | 'secondary';
8
+ requires?: (state: GameState) => boolean;
9
+ }
10
+
11
+ export interface Scene {
12
+ id: string;
13
+ text: string | ((state: GameState) => string);
14
+ choices: SceneChoice[];
15
+ terminal?: boolean;
16
+ }
17
+
18
+ export interface GameState {
19
+ sceneId: string;
20
+ hp: number;
21
+ inventory: string[];
22
+ flags: Record<string, boolean>;
23
+ endingId: string;
24
+ }
25
+
26
+ export interface ChoiceResult {
27
+ nextSceneId: string;
28
+ hpDelta?: number;
29
+ addItem?: string;
30
+ removeItem?: string;
31
+ setFlag?: string;
32
+ endingId?: string;
33
+ }
package/src/story.ts ADDED
@@ -0,0 +1,104 @@
1
+ export { ADV_PREFIX } from './story-types.js';
2
+ export type { SceneChoice, Scene, GameState, ChoiceResult } from './story-types.js';
3
+ export { ITEM_LABELS, ITEM_IDS, itemLabel } from './story-items.js';
4
+ export { SCENES, countScenes } from './story-scenes.js';
5
+
6
+ import type { GameState, ChoiceResult, Scene } from './story-types.js';
7
+ import { itemLabel } from './story-items.js';
8
+ import { SCENES } from './story-scenes.js';
9
+ import { resolveTransition } from './story-transitions.js';
10
+
11
+ export function parseInventory(value: string | string[] | unknown): string[] {
12
+ if (Array.isArray(value)) {
13
+ return value.filter((x): x is string => typeof x === 'string');
14
+ }
15
+ if (typeof value !== 'string' || !value) return [];
16
+ try {
17
+ const v = JSON.parse(value);
18
+ return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ export function parseFlags(value: string | Record<string, boolean> | unknown): Record<string, boolean> {
25
+ if (value != null && typeof value === 'object' && !Array.isArray(value)) {
26
+ return value as Record<string, boolean>;
27
+ }
28
+ if (typeof value !== 'string' || !value) return {};
29
+ try {
30
+ const v = JSON.parse(value);
31
+ return v != null && typeof v === 'object' && !Array.isArray(v)
32
+ ? (v as Record<string, boolean>)
33
+ : {};
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ export function stateFromSession(row: {
40
+ scene_id: string;
41
+ hp: number;
42
+ inventory: string;
43
+ flags: string;
44
+ ending_id: string;
45
+ }): GameState {
46
+ return {
47
+ sceneId: row.scene_id,
48
+ hp: row.hp,
49
+ inventory: parseInventory(row.inventory),
50
+ flags: parseFlags(row.flags),
51
+ endingId: row.ending_id,
52
+ };
53
+ }
54
+
55
+ export function formatStatusBar(state: GameState): string {
56
+ const items = state.inventory.length
57
+ ? state.inventory.map(itemLabel).join('、')
58
+ : '无';
59
+ return `❤️ ${Math.max(0, state.hp)} · 🎒 ${items}`;
60
+ }
61
+
62
+ export function resolveChoice(state: GameState, choiceId: string): ChoiceResult | null {
63
+ return resolveTransition(state, choiceId);
64
+ }
65
+
66
+ export function getScene(sceneId: string): Scene | undefined {
67
+ return SCENES[sceneId];
68
+ }
69
+
70
+ export function visibleChoices(scene: Scene, state: GameState) {
71
+ return scene.choices.filter((c) => !c.requires || c.requires(state));
72
+ }
73
+
74
+ export function applyChoiceResult(state: GameState, result: ChoiceResult): GameState {
75
+ const inventory = [...state.inventory];
76
+ if (result.addItem && !inventory.includes(result.addItem)) {
77
+ inventory.push(result.addItem);
78
+ }
79
+ if (result.removeItem) {
80
+ const idx = inventory.indexOf(result.removeItem);
81
+ if (idx >= 0) inventory.splice(idx, 1);
82
+ }
83
+ const flags = { ...state.flags };
84
+ if (result.setFlag) flags[result.setFlag] = true;
85
+
86
+ const hp = Math.min(100, Math.max(0, state.hp + (result.hpDelta ?? 0)));
87
+
88
+ const nextScene = getScene(result.nextSceneId);
89
+ const endingId = result.endingId ?? (nextScene?.terminal ? result.nextSceneId : '');
90
+
91
+ return {
92
+ sceneId: result.nextSceneId,
93
+ hp,
94
+ inventory,
95
+ flags,
96
+ endingId,
97
+ };
98
+ }
99
+
100
+ export function sceneNarrative(scene: Scene, state: GameState): string {
101
+ const body = typeof scene.text === 'function' ? scene.text(state) : scene.text;
102
+ if (scene.terminal) return body;
103
+ return `${body}\n\n${formatStatusBar(state)}`;
104
+ }