@tapestry-mud/cli 0.1.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.
@@ -0,0 +1,415 @@
1
+ 'use strict';
2
+
3
+ function manifestTemplate(scopedName) {
4
+ return `# Package manifest -- fill in the TODOs before publishing.
5
+ name: "${scopedName}"
6
+ version: "0.1.0"
7
+ type: "module" # core | module | world
8
+ display_name: "TODO: Human-readable name"
9
+ description: "TODO: One-line description for registry search"
10
+ author:
11
+ name: "TODO: Your Name"
12
+ handle: "TODO: your-registry-handle"
13
+ license: "MIT"
14
+
15
+ # Semver range: >=3.0.0 means any engine version at or above this.
16
+ engine: ">=3.0.0"
17
+
18
+ # ^ means compatible minor/patch changes (>=1.0.0 <2.0.0)
19
+ # dependencies:
20
+ # "@scope/pack-name": "^1.0.0"
21
+
22
+ # Optional: warn if not installed, never auto-installed
23
+ # peerDependencies:
24
+ # "@tapestry/sustenance": "^1.0.0"
25
+
26
+ # Capabilities this pack provides (for reverse-dependency lookups)
27
+ provides:
28
+ - example
29
+
30
+ # strict: undeclared tags on entities cause load failure
31
+ # lenient: undeclared tags log warnings, pack still loads
32
+ tag_validation: strict
33
+
34
+ # Path to tag declarations file
35
+ tags: "tags.yml"
36
+
37
+ # Glob patterns -- the engine uses these to find your content
38
+ content:
39
+ areas: "areas/**/area.yaml"
40
+ rooms: "areas/**/rooms/*.yaml"
41
+ items: "areas/**/items/*.yaml"
42
+ mobs: "areas/**/mobs/*.yaml"
43
+ scripts: "scripts/**/*.js"
44
+ help: "help/**/*.yaml"
45
+
46
+ # Discovery metadata (shown by tapestry search and tapestry info)
47
+ meta:
48
+ commands: []
49
+ keywords: ["example"]
50
+ `;
51
+ }
52
+
53
+ function tagsTemplate() {
54
+ return `# Tag declarations for this pack.
55
+ # Tags listed here can be used on entities (items, npcs, rooms, areas).
56
+ # Undeclared tags cause load failure when tag_validation: strict.
57
+ #
58
+ # Convention: always snake_case (e.g., safe_recall, not safe-recall)
59
+ # applies_to: which entity types accept this tag
60
+ # valid values: item, npc, room, area, player
61
+ #
62
+ # Engine tags from @tapestry/core (like killable, no_kill, persistent)
63
+ # are available to all packs without declaring them here.
64
+ # Tags below are YOUR pack's custom tags.
65
+ tags:
66
+ safe_recall:
67
+ description: "Room is a safe recall destination with no combat"
68
+ applies_to: [room]
69
+
70
+ example_tag:
71
+ description: "An example tag -- replace or remove this"
72
+ applies_to: [item]
73
+
74
+ # More examples:
75
+ # cursed:
76
+ # description: "Item carries a curse -- must be removed before unequipping"
77
+ # applies_to: [item]
78
+ # vendor:
79
+ # description: "NPC offers specialized trade goods"
80
+ # applies_to: [npc]
81
+ `;
82
+ }
83
+
84
+ function areaTemplate() {
85
+ return `# Area definition -- one per folder.
86
+ # Areas group rooms, mobs, and items into a named zone.
87
+ area:
88
+ id: example-area # unique within this pack, no spaces
89
+ name: "Example Area" # human-readable name shown in-game
90
+ level_range: [1, 5] # suggested mob level range for this zone
91
+ reset_interval: 1800 # seconds between mob/item respawns
92
+ occupied_modifier: 3.0 # respawn slows by this factor when players are present
93
+ weather_zone: temperate # weather pattern (requires @tapestry/weather)
94
+ flags: [safe_recall] # area flags: city, village, safe_recall, dangerous, safe
95
+ # weather_messages: # custom weather messages for this area
96
+ # storm:
97
+ # start: "Thunder rumbles across the square."
98
+ `;
99
+ }
100
+
101
+ function roomTemplate(shortName) {
102
+ return `# Room definition -- one file per room.
103
+ # ID format: "pack-short-name:room-id" (short name = part after the slash in @scope/name)
104
+ id: "${shortName}:town-square"
105
+ area: example-area # must match area.id in area.yaml
106
+ name: "Town Square"
107
+ description: >
108
+ A cobblestone square at the heart of the example area.
109
+ <npc>A guard</npc> stands watch near the well.
110
+ A <item.uncommon>lantern</item.uncommon> hangs on a hook by the gate.
111
+
112
+ # Exits -- simple or with doors
113
+ exits:
114
+ north: "${shortName}:another-room"
115
+ # Complex exit with a door:
116
+ # south:
117
+ # target: "${shortName}:locked-room"
118
+ # door:
119
+ # name: "an iron gate"
120
+ # closed: true
121
+ # locked: true
122
+ # key: "${shortName}:iron-key"
123
+
124
+ # Tags -- engine tags (safe, recall_point, entry_point, no_wander) are from core
125
+ # Pack tags must be declared in tags.yml
126
+ tags: [safe_recall]
127
+
128
+ properties:
129
+ terrain: city # city, indoors, outdoors, forest, underground, road
130
+ # alignment_range: # restrict entry by alignment
131
+ # max: -500 # only evil players can enter
132
+ # alignment_block_message: "A holy barrier repels you."
133
+
134
+ # Entry point -- marks this room as a starting/recall location
135
+ # entry_point_description: "the town square"
136
+ # entry_point_direction: south
137
+
138
+ # Mobs that spawn here on area reset
139
+ spawns:
140
+ - mob: "${shortName}:example-guard"
141
+ count: 1
142
+ tags: [persistent] # persistent = respawns even while players are present
143
+
144
+ # Items placed in room on reset (not carried by mobs)
145
+ fixtures:
146
+ - "${shortName}:example-lantern"
147
+ `;
148
+ }
149
+
150
+ function mobTemplate(shortName) {
151
+ return `# NPC (mob) definition -- one file per NPC type.
152
+ # ID format: "pack-short-name:mob-id"
153
+ id: "${shortName}:example-guard"
154
+ name: "a guard"
155
+ type: "npc"
156
+
157
+ # Engine tags: killable, no_kill, shop, skill_trainer, vendor, quest, persistent
158
+ # Pack tags must be declared in tags.yml
159
+ tags: [no_kill]
160
+
161
+ # friendly, neutral, hostile -- initial stance toward players
162
+ base_disposition: friendly
163
+
164
+ # Words players type to target this NPC: kill guard, talk guard
165
+ keywords: [guard, soldier]
166
+
167
+ # Behavior -- how the NPC acts when idle
168
+ # behavior: stationary # stationary, wander, patrol, aggro
169
+ # script: "mobs/guide.js" # custom JS behavior script
170
+ # patrol_route: ["${shortName}:room-a", "${shortName}:room-b"]
171
+ # patrol_interval: 60 # seconds between patrol moves
172
+
173
+ stats:
174
+ strength: 12
175
+ dexterity: 10
176
+ constitution: 12
177
+ intelligence: 8
178
+ wisdom: 8
179
+ luck: 6
180
+ max_hp: 100
181
+ max_resource: 0
182
+ max_movement: 100
183
+
184
+ properties:
185
+ level: 5
186
+ description: "A guard standing watch near the gate."
187
+ # gold: 50 # gold dropped on death
188
+ # xp_value: 100 # XP awarded on kill
189
+ # regen_hp: 2.0 # HP regeneration per tick
190
+ # regen_movement: 5 # movement regeneration per tick
191
+ # corpse_decay: 300 # seconds before corpse disappears
192
+
193
+ # Combat properties
194
+ # wimpy_threshold: 15 # % HP to trigger flee
195
+ # ac_slash: 5 # armor class by damage type
196
+ # ac_pierce: 5
197
+ # ac_bash: 5
198
+ # ac_exotic: 0
199
+ # flee_threshold: 0.1 # health % to flee (0.0-1.0)
200
+
201
+ # Idle behavior
202
+ # idle_chance: 0.3 # chance to perform idle action per tick
203
+ # idle_interval: 30 # seconds between idle checks
204
+
205
+ # Wander behavior
206
+ # wander_interval: 45 # seconds between wander moves
207
+ # wander_boundary: "area" # area = stay in area, room = stay put
208
+
209
+ # Dialogue
210
+ # dialogue: "guard-dialogue" # dialogue tree ID
211
+
212
+ # Commands the NPC says/does when idle
213
+ # idle_commands:
214
+ # - "say All quiet on the watch."
215
+ # - "emote scans the square."
216
+
217
+ # Commands the NPC uses in combat
218
+ # battle_commands:
219
+ # - "bash"
220
+ # - "say You dare challenge me?"
221
+
222
+ # Abilities the NPC can use
223
+ # abilities:
224
+ # - id: "bash"
225
+ # proficiency: 75
226
+
227
+ # Items equipped on spawn
228
+ # equipment:
229
+ # - "${shortName}:iron-sword"
230
+
231
+ # Loot dropped on death
232
+ # loot:
233
+ # guaranteed:
234
+ # - item: "${shortName}:guard-badge"
235
+ # count: 1
236
+ # pool:
237
+ # - item: "${shortName}:health-potion"
238
+ # weight: 10
239
+ # - item: "${shortName}:iron-helm"
240
+ # weight: 3
241
+ # pool_rolls: 1
242
+ # rare_bonus:
243
+ # chance: 0.05
244
+ # pool:
245
+ # - item: "${shortName}:rare-blade"
246
+ # weight: 1
247
+
248
+ # NPC trains players (skill_trainer tag required)
249
+ # trains:
250
+ # tier: "apprentice"
251
+ # abilities: ["bash", "parry"]
252
+
253
+ # NPC sells items (shop tag required)
254
+ # properties:
255
+ # shop:
256
+ # sells: ["${shortName}:health-potion", "${shortName}:iron-sword"]
257
+ `;
258
+ }
259
+
260
+ function itemTemplate(shortName) {
261
+ return `# Item definition -- one file per item type.
262
+ # ID format: "pack-short-name:item-id"
263
+ id: "${shortName}:example-lantern"
264
+ name: "a battered lantern"
265
+ type: "item"
266
+
267
+ # Engine tags: consumable, container, fixture, no_get, equippable, fillable,
268
+ # fill_source, readable, emits_light, drinkable, furniture
269
+ # Pack tags must be declared in tags.yml
270
+ tags: [emits_light]
271
+
272
+ # Words players type to target this item: get lantern, look lantern
273
+ keywords: [lantern, light]
274
+
275
+ properties:
276
+ weight: 2
277
+ rarity: common # common, uncommon, rare, epic, artifact
278
+ value: 5 # coin value when sold to a shop
279
+ # description: "A dented lantern that still glows faintly."
280
+ # level: 1 # required/recommended level
281
+
282
+ # Equipment (equippable tag required)
283
+ # slot: light # wield, head, feet, shield, finger, neck, hands, cloak, light, held
284
+
285
+ # Weapon properties (wield slot)
286
+ # damage_dice: "1d6+2" # dice notation
287
+ # hit_bonus: 1
288
+ # attack_speed: 3 # lower = faster
289
+ # combat_name: "slash" # verb for combat messages
290
+ # damage_type: slash # slash, pierce, bash
291
+
292
+ # Armor properties
293
+ # ac_slash: 3
294
+ # ac_pierce: 3
295
+ # ac_bash: 3
296
+ # ac_exotic: 0
297
+
298
+ # Container (container tag required)
299
+ # container_capacity: 10
300
+
301
+ # Consumable (consumable tag required)
302
+ # consume_method: quaff # quaff, drink, eat
303
+ # charges: 3
304
+ # max_charges: 3
305
+ # destroy_on_empty: true
306
+ # effect_id: "heal"
307
+ # effect_duration: 10
308
+ # effect_data:
309
+ # heal_hp: 50
310
+ # sustenance_value: 25 # hunger/thirst satiation
311
+
312
+ # Fillable (fillable tag required)
313
+ # fill_type: "water"
314
+ # fill_source: "water"
315
+
316
+ # Readable (readable tag required)
317
+ # text: "The inscription reads: 'Welcome to the realm.'"
318
+
319
+ # Furniture
320
+ # rest_bonus: 2 # bonus to rest/recovery
321
+
322
+ # Magical essence
323
+ # essence: fire # shadow, fire, earth, storm
324
+
325
+ # Stat modifiers applied when equipped
326
+ # modifiers:
327
+ # - stat: strength # strength, dexterity, constitution, intelligence,
328
+ # value: 2 # wisdom, luck, maxHp, maxMovement, maxResource
329
+ `;
330
+ }
331
+
332
+ function initScriptTemplate(scopedName) {
333
+ return `// init.js -- runs when this pack loads.
334
+ // Register commands, subscribe to events, declare properties.
335
+ // The tapestry object is injected by the engine at load time.
336
+
337
+ // --- Command registration ---
338
+ // Registers a command players can type in-game.
339
+ tapestry.commands.register({
340
+ name: 'example',
341
+ aliases: [],
342
+ description: 'An example command from ${scopedName}',
343
+ category: 'general',
344
+ roles: ['player'],
345
+ args: {
346
+ target: { type: 'text', required: false }
347
+ },
348
+ handler: function(actor, resolved) {
349
+ var msg = resolved.target
350
+ ? 'You examine the ' + resolved.target + '.'
351
+ : 'Nothing to examine.';
352
+ actor.send(msg + '\\r\\n');
353
+ }
354
+ });
355
+
356
+ // --- Event subscriptions ---
357
+ // Subscribe to events from the engine or other packs.
358
+ // Core events: entity:entered_room, entity:left_room,
359
+ // entity:attacked, entity:killed, item:picked_up, item:dropped
360
+ //
361
+ // tapestry.events.on('entity:entered_room', function(entity, room) {
362
+ // var weather = room.get('weather_current');
363
+ // if (weather === 'blizzard') {
364
+ // entity.send('The cold bites at you as you arrive.\\r\\n');
365
+ // }
366
+ // });
367
+
368
+ // --- Property registration ---
369
+ // Declare properties your pack writes to entities.
370
+ // Other packs read these via entity.get('your-property').
371
+ //
372
+ // tapestry.properties.register('example-status', {
373
+ // type: 'string',
374
+ // default: null,
375
+ // applies_to: ['player', 'npc'],
376
+ // });
377
+ `;
378
+ }
379
+
380
+ function helpTemplate(scopedName) {
381
+ return `# Help file -- documents a command or topic.
382
+ # Players read this in-game with: help example
383
+ id: "example"
384
+ title: "Example Command"
385
+ category: "general" # general, combat, social, building, admin
386
+ role: "player" # player, builder, admin (who can see this help)
387
+ keywords: [example, demo]
388
+ brief: "An example command from ${scopedName}."
389
+ syntax:
390
+ - "example"
391
+ - "example [target]"
392
+ body: |
393
+ The example command is a placeholder from the pack scaffold.
394
+ Replace this with documentation for your actual commands.
395
+
396
+ Use syntax entries to show all forms of the command.
397
+ Keep help text concise -- players read this at the terminal.
398
+ see_also: [help, commands]
399
+ `;
400
+ }
401
+
402
+ function generatePackFiles({ scopedName, shortName }) {
403
+ return [
404
+ { path: 'tapestry.yaml', content: manifestTemplate(scopedName) },
405
+ { path: 'tags.yml', content: tagsTemplate() },
406
+ { path: 'areas/example-area/area.yaml', content: areaTemplate() },
407
+ { path: 'areas/example-area/rooms/town-square.yaml', content: roomTemplate(shortName) },
408
+ { path: 'areas/example-area/mobs/guard.yaml', content: mobTemplate(shortName) },
409
+ { path: 'areas/example-area/items/lantern.yaml', content: itemTemplate(shortName) },
410
+ { path: 'scripts/init.js', content: initScriptTemplate(scopedName) },
411
+ { path: 'help/example.yaml', content: helpTemplate(scopedName) },
412
+ ];
413
+ }
414
+
415
+ module.exports = { generatePackFiles };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const { z } = require('zod');
4
+
5
+ const SCOPED_NAME = /^@[a-z0-9-]+\/[a-z0-9-]+$/;
6
+
7
+ const PackageManifestSchema = z.object({
8
+ name: z.string().regex(SCOPED_NAME, 'name must be @scope/package-name'),
9
+ version: z.string().min(1),
10
+ type: z.enum(['core', 'module', 'world']),
11
+ display_name: z.string().min(1),
12
+ description: z.string().min(1),
13
+ author: z.union([
14
+ z.string().min(1),
15
+ z.object({
16
+ name: z.string().min(1),
17
+ handle: z.string().min(1),
18
+ }),
19
+ ]),
20
+ license: z.string().min(1),
21
+ engine: z.string().min(1),
22
+ tag_validation: z.enum(['strict', 'lenient']),
23
+ dependencies: z.record(z.string()).optional(),
24
+ peerDependencies: z.record(z.string()).optional(),
25
+ provides: z.array(z.string()).optional(),
26
+ tags: z.string().optional(),
27
+ module: z.object({
28
+ assembly: z.string(),
29
+ class: z.string(),
30
+ implements: z.string(),
31
+ after: z.string().optional(),
32
+ }).optional(),
33
+ content: z.record(z.string()).optional(),
34
+ client: z.object({
35
+ manifest: z.string(),
36
+ assets: z.string(),
37
+ min_client_version: z.string(),
38
+ }).optional(),
39
+ meta: z.object({
40
+ commands: z.array(z.string()).optional(),
41
+ properties: z.number().optional(),
42
+ keywords: z.array(z.string()).optional(),
43
+ }).optional(),
44
+ });
45
+
46
+ const ProjectManifestSchema = z.object({
47
+ name: z.string().min(1),
48
+ engine: z.union([
49
+ z.string().min(1),
50
+ z.object({
51
+ version: z.string().min(1),
52
+ mode: z.enum(['docker', 'binary', 'source']),
53
+ image: z.string().optional(),
54
+ }),
55
+ ]),
56
+ dependencies: z.record(z.string()).optional(),
57
+ packs: z.array(z.string()).optional(),
58
+ tag_validation: z.enum(['strict', 'lenient']).optional(),
59
+ });
60
+
61
+ function validatePackageManifest(data) {
62
+ return PackageManifestSchema.safeParse(data);
63
+ }
64
+
65
+ function validateProjectManifest(data) {
66
+ return ProjectManifestSchema.safeParse(data);
67
+ }
68
+
69
+ module.exports = {
70
+ PackageManifestSchema,
71
+ ProjectManifestSchema,
72
+ validatePackageManifest,
73
+ validateProjectManifest,
74
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ function createInterface() {
6
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
7
+ }
8
+
9
+ function ask(rl, question) {
10
+ return new Promise((resolve) => {
11
+ rl.question(question, resolve);
12
+ });
13
+ }
14
+
15
+ function askPassword(rl, prompt) {
16
+ return new Promise((resolve) => {
17
+ process.stdout.write(prompt);
18
+ const orig = rl._writeToOutput.bind(rl);
19
+ rl._writeToOutput = () => {};
20
+ rl.question('', (password) => {
21
+ rl._writeToOutput = orig;
22
+ process.stdout.write('\n');
23
+ resolve(password);
24
+ });
25
+ });
26
+ }
27
+
28
+ module.exports = { createInterface, ask, askPassword };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const yaml = require('js-yaml');
4
+ const fs = require('fs');
5
+
6
+ function readYaml(filePath) {
7
+ {
8
+ return yaml.load(fs.readFileSync(filePath, 'utf8'));
9
+ }
10
+ }
11
+
12
+ function writeYaml(filePath, data) {
13
+ {
14
+ fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }));
15
+ }
16
+ }
17
+
18
+ module.exports = { readYaml, writeYaml };