gameforge-cli 0.1.0 → 0.2.1
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.
- package/README.md +117 -44
- package/dist/agents/base/BaseAgent.d.ts +31 -0
- package/dist/agents/base/BaseAgent.d.ts.map +1 -1
- package/dist/agents/base/BaseAgent.js +57 -0
- package/dist/agents/base/BaseAgent.js.map +1 -1
- package/dist/agents/core/Architect.d.ts +21 -5
- package/dist/agents/core/Architect.d.ts.map +1 -1
- package/dist/agents/core/Architect.js +413 -150
- package/dist/agents/core/Architect.js.map +1 -1
- package/dist/agents/core/Chaos.d.ts +4 -0
- package/dist/agents/core/Chaos.d.ts.map +1 -1
- package/dist/agents/core/Chaos.js +46 -11
- package/dist/agents/core/Chaos.js.map +1 -1
- package/dist/agents/core/Consistency.d.ts +1 -0
- package/dist/agents/core/Consistency.d.ts.map +1 -1
- package/dist/agents/core/Consistency.js +86 -11
- package/dist/agents/core/Consistency.js.map +1 -1
- package/dist/agents/core/DocumentUpdater.d.ts +13 -0
- package/dist/agents/core/DocumentUpdater.d.ts.map +1 -0
- package/dist/agents/core/DocumentUpdater.js +165 -0
- package/dist/agents/core/DocumentUpdater.js.map +1 -0
- package/dist/agents/core/Modifier.d.ts +13 -0
- package/dist/agents/core/Modifier.d.ts.map +1 -0
- package/dist/agents/core/Modifier.js +141 -0
- package/dist/agents/core/Modifier.js.map +1 -0
- package/dist/agents/core/Remediation.d.ts +3 -1
- package/dist/agents/core/Remediation.d.ts.map +1 -1
- package/dist/agents/core/Remediation.js +63 -3
- package/dist/agents/core/Remediation.js.map +1 -1
- package/dist/agents/specialists/CreativeSpecialist.d.ts.map +1 -1
- package/dist/agents/specialists/CreativeSpecialist.js +162 -25
- package/dist/agents/specialists/CreativeSpecialist.js.map +1 -1
- package/dist/agents/specialists/EntitySpecialist.d.ts.map +1 -1
- package/dist/agents/specialists/EntitySpecialist.js +79 -25
- package/dist/agents/specialists/EntitySpecialist.js.map +1 -1
- package/dist/agents/specialists/FeatureSpecialist.d.ts +4 -0
- package/dist/agents/specialists/FeatureSpecialist.d.ts.map +1 -1
- package/dist/agents/specialists/FeatureSpecialist.js +114 -39
- package/dist/agents/specialists/FeatureSpecialist.js.map +1 -1
- package/dist/agents/specialists/TechSpecialist.d.ts.map +1 -1
- package/dist/agents/specialists/TechSpecialist.js +169 -32
- package/dist/agents/specialists/TechSpecialist.js.map +1 -1
- package/dist/config/schema.d.ts +1319 -709
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +142 -52
- package/dist/config/schema.js.map +1 -1
- package/dist/config/templates.d.ts.map +1 -1
- package/dist/config/templates.js +6 -66
- package/dist/config/templates.js.map +1 -1
- package/dist/core/Orchestrator.d.ts +17 -3
- package/dist/core/Orchestrator.d.ts.map +1 -1
- package/dist/core/Orchestrator.js +46 -16
- package/dist/core/Orchestrator.js.map +1 -1
- package/dist/index.js +544 -226
- package/dist/index.js.map +1 -1
- package/dist/types/issueReview.d.ts +19 -0
- package/dist/types/issueReview.d.ts.map +1 -0
- package/dist/types/issueReview.js +3 -0
- package/dist/types/issueReview.js.map +1 -0
- package/dist/utils/costTracker.d.ts +28 -0
- package/dist/utils/costTracker.d.ts.map +1 -1
- package/dist/utils/costTracker.js +71 -1
- package/dist/utils/costTracker.js.map +1 -1
- package/dist/utils/disambiguationHelper.d.ts +54 -0
- package/dist/utils/disambiguationHelper.d.ts.map +1 -0
- package/dist/utils/disambiguationHelper.js +262 -0
- package/dist/utils/disambiguationHelper.js.map +1 -0
- package/dist/utils/fileManager.d.ts +7 -0
- package/dist/utils/fileManager.d.ts.map +1 -1
- package/dist/utils/fileManager.js +47 -0
- package/dist/utils/fileManager.js.map +1 -1
- package/dist/utils/issueReviewer.d.ts +10 -0
- package/dist/utils/issueReviewer.d.ts.map +1 -0
- package/dist/utils/issueReviewer.js +206 -0
- package/dist/utils/issueReviewer.js.map +1 -0
- package/dist/utils/issueSelector.d.ts +26 -0
- package/dist/utils/issueSelector.d.ts.map +1 -0
- package/dist/utils/issueSelector.js +132 -0
- package/dist/utils/issueSelector.js.map +1 -0
- package/dist/utils/pdfGenerator.d.ts +12 -0
- package/dist/utils/pdfGenerator.d.ts.map +1 -0
- package/dist/utils/pdfGenerator.js +341 -0
- package/dist/utils/pdfGenerator.js.map +1 -0
- package/package.json +20 -15
- package/dist/core/CheckpointManager.d.ts +0 -16
- package/dist/core/CheckpointManager.d.ts.map +0 -1
- package/dist/core/CheckpointManager.js +0 -52
- package/dist/core/CheckpointManager.js.map +0 -1
|
@@ -32,13 +32,23 @@ Do NOT wrap the JSON in \`\`\`json or \`\`\` tags.`,
|
|
|
32
32
|
return `Q: ${t.question}\nA: ${t.answer}${marker}`;
|
|
33
33
|
})
|
|
34
34
|
.join('\n\n');
|
|
35
|
+
// Check if there are ambiguities that need clarification
|
|
36
|
+
const clarifications = await this.checkForAmbiguities(transcript, onProgress);
|
|
37
|
+
// If there are clarifications, add them to the transcript
|
|
38
|
+
const enhancedTranscript = [...transcript, ...clarifications];
|
|
39
|
+
const enhancedTranscriptStr = enhancedTranscript
|
|
40
|
+
.map(t => {
|
|
41
|
+
const marker = t.autoGenerated ? ' [Auto-Generated]' : '';
|
|
42
|
+
return `Q: ${t.question}\nA: ${t.answer}${marker}`;
|
|
43
|
+
})
|
|
44
|
+
.join('\n\n');
|
|
35
45
|
// Multi-pass generation with constrained prompts
|
|
36
46
|
onProgress?.('Pass 1/3: Generating meta and core features...');
|
|
37
|
-
const metaAndFeatures = await this.generateMetaAndFeatures(
|
|
47
|
+
const metaAndFeatures = await this.generateMetaAndFeatures(enhancedTranscriptStr, onProgress);
|
|
38
48
|
onProgress?.('Pass 2/3: Generating key game objects...');
|
|
39
|
-
const gameObjects = await this.generateGameObjects(
|
|
49
|
+
const gameObjects = await this.generateGameObjects(enhancedTranscriptStr, metaAndFeatures, onProgress);
|
|
40
50
|
onProgress?.('Pass 3/3: Generating creative and technical specs...');
|
|
41
|
-
const creativeAndTech = await this.generateCreativeAndTech(
|
|
51
|
+
const creativeAndTech = await this.generateCreativeAndTech(enhancedTranscriptStr, metaAndFeatures, onProgress);
|
|
42
52
|
// Merge all parts into final GameBible
|
|
43
53
|
onProgress?.('Merging and validating...');
|
|
44
54
|
const gameBible = {
|
|
@@ -48,6 +58,10 @@ Do NOT wrap the JSON in \`\`\`json or \`\`\` tags.`,
|
|
|
48
58
|
creative: creativeAndTech.creative,
|
|
49
59
|
technical: creativeAndTech.technical
|
|
50
60
|
};
|
|
61
|
+
// Check post-generation ambiguities
|
|
62
|
+
await this.checkPostGenerationAmbiguities(gameBible, enhancedTranscript, onProgress);
|
|
63
|
+
// Normalize/coerce common LLM output issues before validation
|
|
64
|
+
this.normalizeGameBible(gameBible);
|
|
51
65
|
try {
|
|
52
66
|
const validated = schema_1.GameBibleSchema.parse(gameBible);
|
|
53
67
|
// Debug logging: Successful validation
|
|
@@ -86,16 +100,320 @@ Do NOT wrap the JSON in \`\`\`json or \`\`\` tags.`,
|
|
|
86
100
|
throw error;
|
|
87
101
|
}
|
|
88
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Normalize/coerce common LLM output issues before schema validation.
|
|
105
|
+
* Fixes issues like strings-instead-of-arrays, missing defaults, etc.
|
|
106
|
+
*/
|
|
107
|
+
normalizeGameBible(bible) {
|
|
108
|
+
// Helper to convert string to array if needed
|
|
109
|
+
const ensureArray = (value) => {
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return value;
|
|
112
|
+
if (typeof value === 'string' && value.trim())
|
|
113
|
+
return [value];
|
|
114
|
+
return [];
|
|
115
|
+
};
|
|
116
|
+
// Normalize features
|
|
117
|
+
if (bible.features) {
|
|
118
|
+
for (const feature of bible.features) {
|
|
119
|
+
// Ensure array fields are arrays
|
|
120
|
+
if (feature.dependencies !== undefined) {
|
|
121
|
+
feature.dependencies = ensureArray(feature.dependencies);
|
|
122
|
+
}
|
|
123
|
+
if (feature.gameplayLoop !== undefined) {
|
|
124
|
+
feature.gameplayLoop = ensureArray(feature.gameplayLoop);
|
|
125
|
+
}
|
|
126
|
+
if (feature.uiRequirements !== undefined) {
|
|
127
|
+
feature.uiRequirements = ensureArray(feature.uiRequirements);
|
|
128
|
+
}
|
|
129
|
+
// Nested optional objects
|
|
130
|
+
if (feature.narrative?.storyBeats !== undefined) {
|
|
131
|
+
feature.narrative.storyBeats = ensureArray(feature.narrative.storyBeats);
|
|
132
|
+
}
|
|
133
|
+
if (feature.narrative?.characters !== undefined) {
|
|
134
|
+
feature.narrative.characters = ensureArray(feature.narrative.characters);
|
|
135
|
+
}
|
|
136
|
+
if (feature.economy?.currencies !== undefined) {
|
|
137
|
+
feature.economy.currencies = ensureArray(feature.economy.currencies);
|
|
138
|
+
}
|
|
139
|
+
if (feature.economy?.balanceFactors !== undefined) {
|
|
140
|
+
feature.economy.balanceFactors = ensureArray(feature.economy.balanceFactors);
|
|
141
|
+
}
|
|
142
|
+
if (feature.progression?.unlockConditions !== undefined) {
|
|
143
|
+
feature.progression.unlockConditions = ensureArray(feature.progression.unlockConditions);
|
|
144
|
+
}
|
|
145
|
+
if (feature.procedural?.constraints !== undefined) {
|
|
146
|
+
feature.procedural.constraints = ensureArray(feature.procedural.constraints);
|
|
147
|
+
}
|
|
148
|
+
if (feature.physics?.interactions !== undefined) {
|
|
149
|
+
feature.physics.interactions = ensureArray(feature.physics.interactions);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Normalize game objects/entities
|
|
154
|
+
if (bible.gameObjects) {
|
|
155
|
+
for (const entity of bible.gameObjects) {
|
|
156
|
+
// Ensure array fields are arrays
|
|
157
|
+
if (entity.narrative?.dialogues !== undefined) {
|
|
158
|
+
entity.narrative.dialogues = ensureArray(entity.narrative.dialogues);
|
|
159
|
+
}
|
|
160
|
+
if (entity.stats?.behaviors !== undefined) {
|
|
161
|
+
entity.stats.behaviors = ensureArray(entity.stats.behaviors);
|
|
162
|
+
}
|
|
163
|
+
if (entity.references?.features !== undefined) {
|
|
164
|
+
entity.references.features = ensureArray(entity.references.features);
|
|
165
|
+
}
|
|
166
|
+
if (entity.references?.relatedEntities !== undefined) {
|
|
167
|
+
entity.references.relatedEntities = ensureArray(entity.references.relatedEntities);
|
|
168
|
+
}
|
|
169
|
+
if (entity.combat?.abilities !== undefined) {
|
|
170
|
+
entity.combat.abilities = ensureArray(entity.combat.abilities);
|
|
171
|
+
}
|
|
172
|
+
if (entity.cardGame?.effects !== undefined) {
|
|
173
|
+
entity.cardGame.effects = ensureArray(entity.cardGame.effects);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Normalize creative section
|
|
178
|
+
if (bible.creative?.referenceLinks !== undefined) {
|
|
179
|
+
bible.creative.referenceLinks = ensureArray(bible.creative.referenceLinks);
|
|
180
|
+
}
|
|
181
|
+
if (bible.creative?.rhythm?.difficulty_levels !== undefined) {
|
|
182
|
+
bible.creative.rhythm.difficulty_levels = ensureArray(bible.creative.rhythm.difficulty_levels);
|
|
183
|
+
}
|
|
184
|
+
if (bible.creative?.rhythm?.note_types !== undefined) {
|
|
185
|
+
bible.creative.rhythm.note_types = ensureArray(bible.creative.rhythm.note_types);
|
|
186
|
+
}
|
|
187
|
+
if (bible.creative?.horror?.scareTechniques !== undefined) {
|
|
188
|
+
bible.creative.horror.scareTechniques = ensureArray(bible.creative.horror.scareTechniques);
|
|
189
|
+
}
|
|
190
|
+
// Normalize technical section
|
|
191
|
+
if (bible.technical?.buildTargets !== undefined) {
|
|
192
|
+
bible.technical.buildTargets = ensureArray(bible.technical.buildTargets);
|
|
193
|
+
}
|
|
194
|
+
if (bible.technical?.localization?.languages !== undefined) {
|
|
195
|
+
bible.technical.localization.languages = ensureArray(bible.technical.localization.languages);
|
|
196
|
+
}
|
|
197
|
+
// Normalize meta section arrays
|
|
198
|
+
if (bible.meta?.genre !== undefined) {
|
|
199
|
+
bible.meta.genre = ensureArray(bible.meta.genre);
|
|
200
|
+
}
|
|
201
|
+
if (bible.meta?.targetPlatform !== undefined) {
|
|
202
|
+
bible.meta.targetPlatform = ensureArray(bible.meta.targetPlatform);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check the transcript for potential ambiguities and ask clarifying questions
|
|
207
|
+
* Returns additional transcript entries with clarifications
|
|
208
|
+
*/
|
|
209
|
+
async checkForAmbiguities(transcript, onProgress) {
|
|
210
|
+
onProgress?.('Checking for ambiguities...');
|
|
211
|
+
const clarifications = [];
|
|
212
|
+
// Check for vague scope
|
|
213
|
+
const scopeAnswer = transcript.find(t => t.question.toLowerCase().includes('scope'))?.answer || '';
|
|
214
|
+
if (scopeAnswer && scopeAnswer.length > 0 &&
|
|
215
|
+
!scopeAnswer.toLowerCase().includes('prototype') &&
|
|
216
|
+
!scopeAnswer.toLowerCase().includes('mvp') &&
|
|
217
|
+
!scopeAnswer.toLowerCase().includes('vertical slice') &&
|
|
218
|
+
!scopeAnswer.toLowerCase().includes('full game')) {
|
|
219
|
+
const result = await this.askClarification('The project scope seems unclear. What is the target development scope?', transcript, 'Scope Clarification');
|
|
220
|
+
clarifications.push({
|
|
221
|
+
question: 'Clarification: Project Scope',
|
|
222
|
+
answer: result.answer,
|
|
223
|
+
autoGenerated: result.wasAIGenerated
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// Check for unclear monetization
|
|
227
|
+
const monetizationAnswer = transcript.find(t => t.question.toLowerCase().includes('monetization') ||
|
|
228
|
+
t.question.toLowerCase().includes('revenue'))?.answer || '';
|
|
229
|
+
const normalizedMonetizationAnswer = monetizationAnswer
|
|
230
|
+
.trim()
|
|
231
|
+
.toLowerCase()
|
|
232
|
+
.replace(/[^\w\s]/g, '');
|
|
233
|
+
if (monetizationAnswer && normalizedMonetizationAnswer === 'custom answer') {
|
|
234
|
+
const result = await this.askClarification('Could you clarify the monetization strategy? How will this game generate revenue?', transcript, 'Monetization Clarification');
|
|
235
|
+
clarifications.push({
|
|
236
|
+
question: 'Clarification: Monetization Details',
|
|
237
|
+
answer: result.answer,
|
|
238
|
+
autoGenerated: result.wasAIGenerated
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Check for vague art style
|
|
242
|
+
const artAnswer = transcript.find(t => t.question.toLowerCase().includes('art') ||
|
|
243
|
+
t.question.toLowerCase().includes('visual'))?.answer || '';
|
|
244
|
+
if (!artAnswer || artAnswer.length < 10) {
|
|
245
|
+
const result = await this.askClarification('The visual art style needs more detail. What specific art direction should we follow?', transcript, 'Art Style Clarification');
|
|
246
|
+
clarifications.push({
|
|
247
|
+
question: 'Clarification: Art Style Details',
|
|
248
|
+
answer: result.answer,
|
|
249
|
+
autoGenerated: result.wasAIGenerated
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Check for vague target audience
|
|
253
|
+
const audienceAnswer = transcript.find(t => t.question.toLowerCase().includes('audience') ||
|
|
254
|
+
t.question.toLowerCase().includes('target'))?.answer || '';
|
|
255
|
+
const normalizedAudience = audienceAnswer.toLowerCase();
|
|
256
|
+
if (audienceAnswer.length < 15 ||
|
|
257
|
+
normalizedAudience.includes('everyone') ||
|
|
258
|
+
normalizedAudience.includes('all ages') ||
|
|
259
|
+
normalizedAudience.includes('casual and hardcore') ||
|
|
260
|
+
normalizedAudience.includes('broad')) {
|
|
261
|
+
const result = await this.askClarification('The target audience is very broad. Who is the PRIMARY audience? (e.g., "competitive FPS players 18-35" vs "everyone")', transcript, 'Target Audience Clarification');
|
|
262
|
+
clarifications.push({
|
|
263
|
+
question: 'Clarification: Target Audience',
|
|
264
|
+
answer: result.answer,
|
|
265
|
+
autoGenerated: result.wasAIGenerated
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// Check for genre mixing overload
|
|
269
|
+
const genreAnswer = transcript.find(t => t.question.toLowerCase().includes('genre'))?.answer || '';
|
|
270
|
+
const genreCount = genreAnswer.split(/[,/&+]/).filter(g => g.trim().length > 0).length;
|
|
271
|
+
if (genreCount > 3) {
|
|
272
|
+
const result = await this.askClarification(`You're mixing ${genreCount} genres. This can dilute the experience. What's the PRIMARY genre?`, transcript, 'Genre Focus');
|
|
273
|
+
clarifications.push({
|
|
274
|
+
question: 'Clarification: Primary Genre',
|
|
275
|
+
answer: result.answer,
|
|
276
|
+
autoGenerated: result.wasAIGenerated
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (clarifications.length > 0) {
|
|
280
|
+
onProgress?.(`Added ${clarifications.length} clarifications`);
|
|
281
|
+
}
|
|
282
|
+
return clarifications;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Check the generated GameBible for structural issues and ask clarifying questions
|
|
286
|
+
*/
|
|
287
|
+
async checkPostGenerationAmbiguities(bible, transcript, onProgress) {
|
|
288
|
+
// Check for too many features for scope
|
|
289
|
+
const scope = bible.meta.estimatedScope;
|
|
290
|
+
const featureCount = bible.features.length;
|
|
291
|
+
const featureLimits = {
|
|
292
|
+
'Prototype': 5,
|
|
293
|
+
'Vertical Slice': 8,
|
|
294
|
+
'MVP': 12,
|
|
295
|
+
'Full Game': 20
|
|
296
|
+
};
|
|
297
|
+
const limit = featureLimits[scope] || 12;
|
|
298
|
+
if (featureCount > limit) {
|
|
299
|
+
const result = await this.askClarification(`You have ${featureCount} features for a "${scope}" project. This may be too ambitious. Should we prioritize the most critical features?`, bible, 'Feature Count');
|
|
300
|
+
// Note: We inform but don't auto-reduce features - user decides
|
|
301
|
+
onProgress?.(`Feature count acknowledged: ${featureCount} features for ${scope} scope`);
|
|
302
|
+
}
|
|
303
|
+
// Check for core gameplay loop
|
|
304
|
+
const hasGameplayLoop = bible.features.some((f) => f.gameplayLoop && f.gameplayLoop.length > 0);
|
|
305
|
+
if (!hasGameplayLoop) {
|
|
306
|
+
const result = await this.askClarification('No gameplay loops are defined. What does the player DO moment-to-moment in this game?', bible, 'Core Gameplay Loop');
|
|
307
|
+
onProgress?.('Core gameplay loop clarified');
|
|
308
|
+
}
|
|
309
|
+
// Check for genre-specific features
|
|
310
|
+
const genres = bible.meta.genre.map((g) => g.toLowerCase());
|
|
311
|
+
// RPG without progression
|
|
312
|
+
if (genres.some((g) => g.includes('rpg')) &&
|
|
313
|
+
!bible.features.some((f) => f.progression)) {
|
|
314
|
+
const result = await this.askClarification('RPG games typically need a progression system. Should we add character/player progression?', bible, 'RPG Progression');
|
|
315
|
+
const decision = (result.answer || '').toLowerCase();
|
|
316
|
+
if (decision.includes('yes') || decision.includes('add') || decision.includes('should')) {
|
|
317
|
+
onProgress?.('Note: User confirmed RPG progression system should be added - consider this for future feature expansion');
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
onProgress?.('RPG progression acknowledged as intentional design decision');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Multiplayer genre without networking
|
|
324
|
+
if ((genres.some((g) => g.includes('multiplayer') || g.includes('mmo') || g.includes('competitive')) ||
|
|
325
|
+
bible.features.some((f) => f.multiplayer)) &&
|
|
326
|
+
!bible.technical?.networking) {
|
|
327
|
+
const result = await this.askClarification('Multiplayer features detected but no networking architecture specified. What network model should we use?', bible, 'Multiplayer Networking');
|
|
328
|
+
const decision = (result.answer || '').toLowerCase();
|
|
329
|
+
// Extract network architecture from user response
|
|
330
|
+
if (!bible.technical) {
|
|
331
|
+
bible.technical = { engine: { primary: 'Unity', version: '2023.2', reasoning: 'Default engine' }, buildTargets: [] };
|
|
332
|
+
}
|
|
333
|
+
if (decision.includes('client-server') || decision.includes('client server')) {
|
|
334
|
+
bible.technical.networking = { architecture: 'Client-Server', maxPlayers: 8 };
|
|
335
|
+
onProgress?.('Added Client-Server networking architecture');
|
|
336
|
+
}
|
|
337
|
+
else if (decision.includes('peer-to-peer') || decision.includes('p2p')) {
|
|
338
|
+
bible.technical.networking = { architecture: 'Peer-to-Peer', maxPlayers: 4 };
|
|
339
|
+
onProgress?.('Added Peer-to-Peer networking architecture');
|
|
340
|
+
}
|
|
341
|
+
else if (decision.includes('relay')) {
|
|
342
|
+
bible.technical.networking = { architecture: 'Relay Server', maxPlayers: 8 };
|
|
343
|
+
onProgress?.('Added Relay Server networking architecture');
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
onProgress?.('Multiplayer networking clarified - manual specification may be needed');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Card game without cards
|
|
350
|
+
if (genres.some((g) => g.includes('card') || g.includes('ccg') || g.includes('tcg')) &&
|
|
351
|
+
!bible.gameObjects.some((e) => e.category === 'Card')) {
|
|
352
|
+
const result = await this.askClarification('Card game genre but no card entities defined. Should we add card definitions?', bible, 'Card Game Entities');
|
|
353
|
+
const decision = (result.answer || '').toLowerCase();
|
|
354
|
+
if (decision.includes('yes') || decision.includes('add') || decision.includes('should')) {
|
|
355
|
+
onProgress?.('Note: User confirmed cards should be added - consider expanding entity list');
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
onProgress?.('Card-less design acknowledged as intentional');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Horror without atmosphere
|
|
362
|
+
if (genres.some((g) => g.includes('horror')) &&
|
|
363
|
+
!bible.creative?.horror?.atmosphere) {
|
|
364
|
+
const result = await this.askClarification('Horror game but no horror atmosphere defined. What kind of horror experience are you creating?', bible, 'Horror Atmosphere');
|
|
365
|
+
const decision = (result.answer || '').toLowerCase();
|
|
366
|
+
// Attempt to add horror atmosphere based on user response
|
|
367
|
+
if (bible.creative && decision.length > 10) {
|
|
368
|
+
if (!bible.creative.horror) {
|
|
369
|
+
bible.creative.horror = {};
|
|
370
|
+
}
|
|
371
|
+
const horror = bible.creative.horror;
|
|
372
|
+
// Extract atmosphere keywords from response
|
|
373
|
+
if (decision.includes('psychological')) {
|
|
374
|
+
horror.atmosphere = 'Psychological tension and unease';
|
|
375
|
+
}
|
|
376
|
+
else if (decision.includes('survival')) {
|
|
377
|
+
horror.atmosphere = 'Survival horror with resource scarcity';
|
|
378
|
+
}
|
|
379
|
+
else if (decision.includes('jump scare') || decision.includes('jumpscare')) {
|
|
380
|
+
horror.atmosphere = 'Jump scares and sudden frights';
|
|
381
|
+
}
|
|
382
|
+
else if (decision.includes('cosmic') || decision.includes('lovecraft')) {
|
|
383
|
+
horror.atmosphere = 'Cosmic horror and existential dread';
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
horror.atmosphere = result.answer.substring(0, 100);
|
|
387
|
+
}
|
|
388
|
+
onProgress?.('Added horror atmosphere based on user input');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Story/narrative game without NPCs
|
|
392
|
+
if ((genres.some((g) => g.includes('rpg') || g.includes('adventure') || g.includes('story')) ||
|
|
393
|
+
bible.features.some((f) => f.narrative)) &&
|
|
394
|
+
!bible.gameObjects.some((e) => e.category === 'NPC')) {
|
|
395
|
+
const result = await this.askClarification('Story-driven game with no NPCs. Should we add key characters to populate the world?', bible, 'Story Characters');
|
|
396
|
+
const decision = (result.answer || '').toLowerCase();
|
|
397
|
+
if (decision.includes('yes') || decision.includes('add') || decision.includes('should')) {
|
|
398
|
+
onProgress?.('Note: User confirmed NPCs should be added - consider expanding entity list');
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
onProgress?.('NPC-less design acknowledged as intentional (e.g., environmental storytelling)');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
89
405
|
async generateMetaAndFeatures(transcriptStr, onProgress) {
|
|
90
406
|
const prompt = `Generate the meta section and core features for a Game Design Document.
|
|
91
407
|
|
|
92
408
|
INTERVIEW:
|
|
93
409
|
${transcriptStr}
|
|
94
410
|
|
|
95
|
-
CONSTRAINTS - Keep it focused:
|
|
411
|
+
CONSTRAINTS - Keep it focused on DESIGN, not implementation:
|
|
96
412
|
- Generate 3-8 core features maximum (prioritize the most important systems)
|
|
97
|
-
-
|
|
413
|
+
- Focus on what the feature DOES for the player, not how it's coded
|
|
414
|
+
- Describe gameplay loops in plain language
|
|
98
415
|
- Limit to essential dependencies only
|
|
416
|
+
- ONLY include fields that are relevant to the genre - omit optional fields that don't apply
|
|
99
417
|
|
|
100
418
|
OUTPUT STRUCTURE (use this EXACT format):
|
|
101
419
|
{
|
|
@@ -114,44 +432,36 @@ OUTPUT STRUCTURE (use this EXACT format):
|
|
|
114
432
|
{
|
|
115
433
|
"id": "FEAT-001",
|
|
116
434
|
"name": "Feature Name",
|
|
117
|
-
"intent": "Why this feature exists",
|
|
118
|
-
"dependencies": [
|
|
119
|
-
"gameplayLoop": ["
|
|
120
|
-
"uiRequirements": ["
|
|
435
|
+
"intent": "Why this feature exists and what it adds to player experience",
|
|
436
|
+
"dependencies": [],
|
|
437
|
+
"gameplayLoop": ["Player does X", "System responds with Y", "Player decides Z"],
|
|
438
|
+
"uiRequirements": ["Health bar", "Inventory panel"],
|
|
121
439
|
"technical": {
|
|
122
|
-
"dataStructure": "struct FeatureName { field: type; }",
|
|
123
|
-
"mathFormulas": [
|
|
124
|
-
{
|
|
125
|
-
"expression": "damage = baseDamage * (1 + critChance)",
|
|
126
|
-
"variables": {
|
|
127
|
-
"baseDamage": { "type": "float", "range": "1.0-100.0" },
|
|
128
|
-
"critChance": { "type": "float", "range": "0.0-1.0" }
|
|
129
|
-
},
|
|
130
|
-
"validated": false,
|
|
131
|
-
"balanceNotes": "Adjust critChance if damage spikes too high"
|
|
132
|
-
}
|
|
133
|
-
],
|
|
134
|
-
"fileLocation": "src/features/FeatureName/",
|
|
135
440
|
"estimatedComplexity": "Medium"
|
|
136
441
|
},
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"As a player, I want to... so that..."
|
|
141
|
-
],
|
|
142
|
-
"acceptanceCriteria": [
|
|
143
|
-
"Given... when... then..."
|
|
144
|
-
]
|
|
442
|
+
"multiplayer": {
|
|
443
|
+
"playerCount": "2-8",
|
|
444
|
+
"networkModel": "Client-Server"
|
|
145
445
|
}
|
|
146
446
|
}
|
|
147
447
|
]
|
|
148
448
|
}
|
|
149
449
|
|
|
150
|
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
450
|
+
OPTIONAL FIELDS - Include ONLY if relevant to the game genre:
|
|
451
|
+
- features[].gameplayLoop - OPTIONAL for non-gameplay features (e.g., settings, menus)
|
|
452
|
+
- features[].monetization - OPTIONAL for games with monetization (strategy only)
|
|
453
|
+
- features[].multiplayer - OPTIONAL for multiplayer features (playerCount as string, networkModel)
|
|
454
|
+
- features[].narrative - OPTIONAL for story-driven features (storyBeats, characters)
|
|
455
|
+
- features[].economy - OPTIONAL for games with currency/trading (currencies, tradeable)
|
|
456
|
+
- features[].progression - OPTIONAL for games with leveling/unlocks (unlockConditions, skillTrees)
|
|
457
|
+
- features[].procedural - OPTIONAL for procedural generation features (algorithm name only)
|
|
458
|
+
- features[].physics - OPTIONAL for physics-heavy features (realism level only)
|
|
459
|
+
|
|
460
|
+
IMPORTANT: This is a DESIGN document, not an engineering spec. Do NOT include:
|
|
461
|
+
- Code or pseudo-code
|
|
462
|
+
- File paths or directory structures
|
|
463
|
+
- Detailed math formulas with variable types
|
|
464
|
+
- Implementation details
|
|
155
465
|
|
|
156
466
|
Output ONLY the JSON, no markdown blocks.`;
|
|
157
467
|
const response = await this.callLLMWithAutoRetry(prompt, modelSelector_1.TaskComplexity.COMPLEX, {
|
|
@@ -170,50 +480,28 @@ ${transcriptStr}
|
|
|
170
480
|
FEATURES:
|
|
171
481
|
${JSON.stringify(metaAndFeatures.features.map(f => ({ id: f.id, name: f.name })), null, 2)}
|
|
172
482
|
|
|
173
|
-
CONSTRAINTS -
|
|
483
|
+
CONSTRAINTS - Keep it design-focused:
|
|
174
484
|
- Generate 5-15 game objects maximum (most important entities only)
|
|
175
485
|
- For card games: include a few representative cards, not every single card
|
|
176
|
-
- Keep backstories
|
|
177
|
-
-
|
|
486
|
+
- Keep backstories brief (1-2 sentences max)
|
|
487
|
+
- Focus on what the entity IS and what it DOES, not technical implementation
|
|
488
|
+
- ONLY include fields that are relevant to the entity type
|
|
178
489
|
|
|
179
490
|
OUTPUT STRUCTURE (use this EXACT format):
|
|
180
491
|
[
|
|
181
492
|
{
|
|
182
493
|
"id": "NPC-001",
|
|
183
494
|
"name": "Character Name",
|
|
184
|
-
"category": "NPC",
|
|
495
|
+
"category": "NPC",
|
|
185
496
|
"narrative": {
|
|
186
|
-
"backstory": "Brief 1-2 sentence backstory"
|
|
187
|
-
"dialogues": ["Dialogue line 1", "Dialogue line 2"],
|
|
188
|
-
"externalData": {
|
|
189
|
-
"type": "Spreadsheet",
|
|
190
|
-
"schema": "columns: name (string), dialogueKey (string), emotion (string)",
|
|
191
|
-
"sampleRows": 5,
|
|
192
|
-
"fileReference": "npcs/character_name_dialogues.csv"
|
|
193
|
-
}
|
|
497
|
+
"backstory": "Brief 1-2 sentence backstory"
|
|
194
498
|
},
|
|
195
499
|
"stats": {
|
|
196
500
|
"attributes": {
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"AttackPower": 25
|
|
501
|
+
"Role": "Merchant",
|
|
502
|
+
"Personality": "Friendly"
|
|
200
503
|
},
|
|
201
|
-
"behaviors": ["
|
|
202
|
-
"balanceFormulas": [
|
|
203
|
-
{
|
|
204
|
-
"expression": "effectiveHP = HP * (1 + armor * 0.01)",
|
|
205
|
-
"variables": {
|
|
206
|
-
"HP": { "type": "int", "range": "1-1000" },
|
|
207
|
-
"armor": { "type": "int", "range": "0-100" }
|
|
208
|
-
},
|
|
209
|
-
"validated": false,
|
|
210
|
-
"balanceNotes": "Ensure armor doesn't make character invincible"
|
|
211
|
-
}
|
|
212
|
-
]
|
|
213
|
-
},
|
|
214
|
-
"references": {
|
|
215
|
-
"features": ["FEAT-001"],
|
|
216
|
-
"relatedEntities": ["ITEM-005"]
|
|
504
|
+
"behaviors": ["sells items", "gives quests"]
|
|
217
505
|
}
|
|
218
506
|
},
|
|
219
507
|
{
|
|
@@ -222,35 +510,30 @@ OUTPUT STRUCTURE (use this EXACT format):
|
|
|
222
510
|
"category": "Item",
|
|
223
511
|
"stats": {
|
|
224
512
|
"attributes": {
|
|
225
|
-
"
|
|
226
|
-
"
|
|
513
|
+
"Type": "Consumable",
|
|
514
|
+
"Effect": "Restores health"
|
|
227
515
|
},
|
|
228
|
-
"behaviors": ["
|
|
229
|
-
},
|
|
230
|
-
"references": {
|
|
231
|
-
"features": ["FEAT-003"],
|
|
232
|
-
"relatedEntities": []
|
|
516
|
+
"behaviors": ["single use"]
|
|
233
517
|
}
|
|
234
518
|
}
|
|
235
519
|
]
|
|
236
520
|
|
|
237
|
-
CRITICAL -
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
- NEVER use true/false for attributes
|
|
521
|
+
CRITICAL - Category must be one of these EXACT values:
|
|
522
|
+
- "NPC", "Monster", "Item", "Interactable", "Vehicle", "Card", "Building", "Ability"
|
|
523
|
+
|
|
524
|
+
OPTIONAL FIELDS - Include ONLY if relevant:
|
|
525
|
+
- narrative - OPTIONAL - brief backstory only
|
|
526
|
+
- stats - OPTIONAL - simple attributes and behaviors
|
|
527
|
+
- combat - OPTIONAL for combat entities (health, damage, armor)
|
|
528
|
+
- social - OPTIONAL for NPCs (relationship type, questGiver boolean)
|
|
529
|
+
- cardGame - OPTIONAL for cards (cost, effects, cardType)
|
|
530
|
+
- ai - OPTIONAL for AI entities (aiType like "Passive" or "Aggressive")
|
|
531
|
+
|
|
532
|
+
IMPORTANT - This is a DESIGN document:
|
|
533
|
+
- Use descriptive attributes, not numeric RPG stats unless the game is an RPG
|
|
534
|
+
- Do NOT include balance formulas or detailed math
|
|
535
|
+
- Do NOT include detection radiuses, behavior trees, or AI implementation details
|
|
536
|
+
- Focus on the entity's role in the game, not how it's programmed
|
|
254
537
|
|
|
255
538
|
Output ONLY the JSON array.`;
|
|
256
539
|
const response = await this.callLLMWithAutoRetry(prompt, modelSelector_1.TaskComplexity.COMPLEX, {
|
|
@@ -261,22 +544,22 @@ Output ONLY the JSON array.`;
|
|
|
261
544
|
return this.parseJSON(response.content);
|
|
262
545
|
}
|
|
263
546
|
async generateCreativeAndTech(transcriptStr, metaAndFeatures, onProgress) {
|
|
264
|
-
const prompt = `Generate creative and technical sections.
|
|
547
|
+
const prompt = `Generate creative vision and technical overview sections.
|
|
265
548
|
|
|
266
549
|
GAME: ${metaAndFeatures.meta.title}
|
|
267
550
|
GENRE: ${metaAndFeatures.meta.genre.join(', ')}
|
|
268
551
|
|
|
269
|
-
CONSTRAINTS -
|
|
270
|
-
- Art style: 2-3 sentences
|
|
271
|
-
- Audio mood: 2-3 sentences
|
|
272
|
-
- Keep asset counts realistic
|
|
552
|
+
CONSTRAINTS - Focus on creative vision and high-level technical decisions:
|
|
553
|
+
- Art style: 2-3 sentences describing the visual mood and aesthetic
|
|
554
|
+
- Audio mood: 2-3 sentences describing the sound and music direction
|
|
555
|
+
- Keep asset counts realistic estimates
|
|
556
|
+
- Technical section should be a brief overview, not an engineering spec
|
|
273
557
|
|
|
274
558
|
OUTPUT STRUCTURE (use this EXACT format):
|
|
275
559
|
{
|
|
276
560
|
"creative": {
|
|
277
|
-
"artStyle": "2-3 sentence description of visual style",
|
|
278
|
-
"audioMood": "2-3 sentence description of
|
|
279
|
-
"referenceLinks": ["https://example.com/reference1"],
|
|
561
|
+
"artStyle": "2-3 sentence description of visual style, mood, and aesthetic influences",
|
|
562
|
+
"audioMood": "2-3 sentence description of music style and sound design direction",
|
|
280
563
|
"assetRequirements": {
|
|
281
564
|
"characters": 10,
|
|
282
565
|
"environments": 5,
|
|
@@ -284,48 +567,44 @@ OUTPUT STRUCTURE (use this EXACT format):
|
|
|
284
567
|
"ui": 15,
|
|
285
568
|
"sfx": 30,
|
|
286
569
|
"music": 8
|
|
287
|
-
},
|
|
288
|
-
"pipeline": {
|
|
289
|
-
"toolsRequired": ["Blender", "Photoshop", "Audacity"],
|
|
290
|
-
"workflowNotes": "Brief workflow description"
|
|
291
570
|
}
|
|
292
571
|
},
|
|
293
572
|
"technical": {
|
|
294
573
|
"engine": {
|
|
295
|
-
"primary": "
|
|
296
|
-
"version": "
|
|
297
|
-
"
|
|
298
|
-
"reasoning": "Why this engine was chosen"
|
|
574
|
+
"primary": "Unity",
|
|
575
|
+
"version": "2023.2",
|
|
576
|
+
"reasoning": "Brief explanation of why this engine fits the project"
|
|
299
577
|
},
|
|
300
|
-
"toolsRequired": [
|
|
301
|
-
{
|
|
302
|
-
"name": "Visual Studio",
|
|
303
|
-
"purpose": "C++ development",
|
|
304
|
-
"required": true
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
"name": "Git",
|
|
308
|
-
"purpose": "Version control",
|
|
309
|
-
"required": true
|
|
310
|
-
}
|
|
311
|
-
],
|
|
312
|
-
"localization": {
|
|
313
|
-
"strategy": "English Only",
|
|
314
|
-
"languages": ["en"],
|
|
315
|
-
"stringCount": 500
|
|
316
|
-
},
|
|
317
|
-
"directoryStructure": "Assets/\n Characters/\n Environments/\nSource/\n Core/\n Features/",
|
|
318
578
|
"buildTargets": ["Windows", "Mac"]
|
|
319
579
|
}
|
|
320
580
|
}
|
|
321
581
|
|
|
322
|
-
|
|
582
|
+
OPTIONAL FIELDS - Include ONLY if relevant:
|
|
583
|
+
Creative:
|
|
584
|
+
- assetRequirements.vehicles - OPTIONAL for games with vehicles
|
|
585
|
+
- assetRequirements.cards - OPTIONAL for card games
|
|
586
|
+
- assetRequirements.animations - OPTIONAL for animation-heavy games
|
|
587
|
+
- visualNovel - OPTIONAL for visual novel games
|
|
588
|
+
- rhythm - OPTIONAL for rhythm games
|
|
589
|
+
- horror - OPTIONAL for horror games (atmosphere, lighting mood)
|
|
590
|
+
|
|
591
|
+
Technical:
|
|
592
|
+
- localization - OPTIONAL (strategy and languages only)
|
|
593
|
+
- networking - OPTIONAL for multiplayer (architecture and maxPlayers only)
|
|
594
|
+
- accessibility - OPTIONAL (colorblindMode, subtitles, remappableControls, difficultyOptions)
|
|
595
|
+
|
|
596
|
+
CRITICAL:
|
|
323
597
|
- engine.primary MUST be one of: "Unreal Engine 5", "Unity", "Godot", "Custom"
|
|
324
|
-
-
|
|
325
|
-
- localization.strategy MUST be: "None", "English Only", "EFIGS", or "Global"
|
|
326
|
-
- localization.languages is OPTIONAL but recommended (use ["en"] for English Only)
|
|
598
|
+
- localization.strategy (if included) MUST be: "None", "English Only", "EFIGS", or "Global"
|
|
327
599
|
- buildTargets MUST use exact values: "Windows", "Mac", "Linux", "iOS", "Android", "Console"
|
|
328
600
|
|
|
601
|
+
DO NOT INCLUDE (these are implementation details, not design):
|
|
602
|
+
- Production pipeline or workflow
|
|
603
|
+
- Tool lists or software requirements
|
|
604
|
+
- Directory structures
|
|
605
|
+
- Tick rates, bandwidth, or networking implementation details
|
|
606
|
+
- Performance targets or scalability settings
|
|
607
|
+
|
|
329
608
|
Output ONLY the JSON.`;
|
|
330
609
|
const response = await this.callLLMWithAutoRetry(prompt, modelSelector_1.TaskComplexity.COMPLEX, {
|
|
331
610
|
initialMaxTokens: 16000,
|
|
@@ -439,45 +718,29 @@ Output ONLY the JSON.`;
|
|
|
439
718
|
"name": "string",
|
|
440
719
|
"intent": "string",
|
|
441
720
|
"dependencies": ["FEAT-XXX"],
|
|
442
|
-
"monetization": { "strategy": "None|IAP|Premium|Ads|Hybrid", "implementation": "string" },
|
|
443
721
|
"gameplayLoop": ["step1", "step2"],
|
|
444
722
|
"uiRequirements": ["string"],
|
|
445
723
|
"technical": {
|
|
446
|
-
"dataStructure": "pseudo-code string",
|
|
447
|
-
"mathFormulas": [{ "expression": "string", "variables": {"name": {"type": "int|float|bool", "range": "string"}}, "validated": false, "balanceNotes": "string" }],
|
|
448
|
-
"fileLocation": "path/to/files",
|
|
449
724
|
"estimatedComplexity": "Low|Medium|High|Very High"
|
|
450
|
-
},
|
|
451
|
-
"agile": {
|
|
452
|
-
"epic": "EPIC-XXX: Name",
|
|
453
|
-
"userStories": ["As a player, I want..."],
|
|
454
|
-
"acceptanceCriteria": ["Criteria"]
|
|
455
725
|
}
|
|
456
726
|
}],
|
|
457
727
|
"gameObjects": [{
|
|
458
728
|
"id": "NPC-XXX|ITEM-XXX|MON-XXX",
|
|
459
729
|
"name": "string",
|
|
460
730
|
"category": "NPC|Monster|Item|Interactable",
|
|
461
|
-
"narrative": { "backstory": "string"
|
|
731
|
+
"narrative": { "backstory": "string" },
|
|
462
732
|
"stats": {
|
|
463
|
-
"attributes": {"
|
|
464
|
-
"behaviors": ["string"]
|
|
465
|
-
|
|
466
|
-
},
|
|
467
|
-
"references": { "features": ["FEAT-XXX"], "relatedEntities": ["ENTITY-XXX"] }
|
|
733
|
+
"attributes": {"Role": "Merchant", "Type": "Friendly"},
|
|
734
|
+
"behaviors": ["string"]
|
|
735
|
+
}
|
|
468
736
|
}],
|
|
469
737
|
"creative": {
|
|
470
738
|
"artStyle": "string",
|
|
471
739
|
"audioMood": "string",
|
|
472
|
-
"
|
|
473
|
-
"assetRequirements": { "characters": 0, "environments": 0, "props": 0, "ui": 0, "sfx": 0, "music": 0 },
|
|
474
|
-
"pipeline": { "toolsRequired": ["Tool"], "workflowNotes": "string" }
|
|
740
|
+
"assetRequirements": { "characters": 0, "environments": 0, "props": 0, "ui": 0, "sfx": 0, "music": 0 }
|
|
475
741
|
},
|
|
476
742
|
"technical": {
|
|
477
|
-
"engine": { "primary": "Unreal Engine 5|Unity|Godot|Custom", "version": "string", "
|
|
478
|
-
"toolsRequired": [{ "name": "Tool", "purpose": "string", "required": true }],
|
|
479
|
-
"localization": { "strategy": "None|English Only|EFIGS|Global", "languages": ["en"], "stringCount": 0 },
|
|
480
|
-
"directoryStructure": "path structure",
|
|
743
|
+
"engine": { "primary": "Unreal Engine 5|Unity|Godot|Custom", "version": "string", "reasoning": "string" },
|
|
481
744
|
"buildTargets": ["Windows", "Mac", "Linux", "iOS", "Android", "Console"]
|
|
482
745
|
}
|
|
483
746
|
}`;
|