rpg-event-generator 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.
- package/LICENSE +21 -0
- package/README.md +430 -0
- package/dist/index.js +1144 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.RPGEventGenerator = void 0;
|
|
7
|
+
const {
|
|
8
|
+
Chance
|
|
9
|
+
} = require('chance');
|
|
10
|
+
class SimpleMarkovGenerator {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.stateSize = options.stateSize || 2;
|
|
13
|
+
this.data = [];
|
|
14
|
+
this.chain = new Map();
|
|
15
|
+
}
|
|
16
|
+
addData(texts) {
|
|
17
|
+
this.data = this.data.concat(texts);
|
|
18
|
+
this.buildChain();
|
|
19
|
+
}
|
|
20
|
+
buildChain() {
|
|
21
|
+
this.chain.clear();
|
|
22
|
+
this.data.forEach(text => {
|
|
23
|
+
const words = text.split(/\s+/);
|
|
24
|
+
for (let i = 0; i <= words.length - this.stateSize; i++) {
|
|
25
|
+
const state = words.slice(i, i + this.stateSize).join(' ');
|
|
26
|
+
const nextWord = words[i + this.stateSize];
|
|
27
|
+
if (!this.chain.has(state)) {
|
|
28
|
+
this.chain.set(state, []);
|
|
29
|
+
}
|
|
30
|
+
if (nextWord) {
|
|
31
|
+
this.chain.get(state).push(nextWord);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
generate(options = {}) {
|
|
37
|
+
const minLength = options.minLength || 20;
|
|
38
|
+
const maxLength = options.maxLength || 100;
|
|
39
|
+
const maxTries = options.maxTries || 10;
|
|
40
|
+
for (let tries = 0; tries < maxTries; tries++) {
|
|
41
|
+
const result = this.generateAttempt(minLength, maxLength);
|
|
42
|
+
if (result) {
|
|
43
|
+
return {
|
|
44
|
+
string: result
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const fragments = [];
|
|
49
|
+
const numFragments = Math.floor(Math.random() * 2) + 2;
|
|
50
|
+
for (let i = 0; i < numFragments; i++) {
|
|
51
|
+
const fragment = this.data[Math.floor(Math.random() * this.data.length)];
|
|
52
|
+
if (fragment && !fragments.includes(fragment)) {
|
|
53
|
+
fragments.push(fragment);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let combined = fragments.join('. ');
|
|
57
|
+
if (!combined.endsWith('.')) combined += '.';
|
|
58
|
+
return {
|
|
59
|
+
string: combined
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
generateAttempt(minLength, maxLength) {
|
|
63
|
+
const states = Array.from(this.chain.keys());
|
|
64
|
+
if (states.length === 0) return null;
|
|
65
|
+
let currentState = states[Math.floor(Math.random() * states.length)];
|
|
66
|
+
const words = currentState.split(' ');
|
|
67
|
+
let attempts = 0;
|
|
68
|
+
const maxAttempts = 100;
|
|
69
|
+
while (words.join(' ').length < maxLength && attempts < maxAttempts) {
|
|
70
|
+
const nextWords = this.chain.get(currentState);
|
|
71
|
+
if (!nextWords || nextWords.length === 0) {
|
|
72
|
+
const similarStates = states.filter(s => s.split(' ')[0] === words[words.length - 1]);
|
|
73
|
+
if (similarStates.length > 0) {
|
|
74
|
+
currentState = similarStates[Math.floor(Math.random() * similarStates.length)];
|
|
75
|
+
const stateWords = currentState.split(' ');
|
|
76
|
+
words.push(...stateWords.slice(1));
|
|
77
|
+
} else {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const nextWord = nextWords[Math.floor(Math.random() * nextWords.length)];
|
|
82
|
+
words.push(nextWord);
|
|
83
|
+
currentState = words.slice(-this.stateSize).join(' ');
|
|
84
|
+
}
|
|
85
|
+
attempts++;
|
|
86
|
+
}
|
|
87
|
+
const result = words.join(' ');
|
|
88
|
+
let finalResult = result.charAt(0).toUpperCase() + result.slice(1);
|
|
89
|
+
if (!finalResult.endsWith('.') && !finalResult.endsWith('!') && !finalResult.endsWith('?')) {
|
|
90
|
+
finalResult += '.';
|
|
91
|
+
}
|
|
92
|
+
return finalResult.length >= minLength && finalResult.length <= maxLength ? finalResult : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if generated text is interesting enough to use
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
isInterestingText(text) {
|
|
100
|
+
const boringWords = ['ordinary', 'normal', 'regular', 'simple', 'plain', 'basic', 'usual', 'typical', 'common', 'standard', 'boring', 'mundane', 'dull'];
|
|
101
|
+
const interestingWords = ['mysterious', 'ancient', 'powerful', 'dark', 'forbidden', 'legendary', 'cursed', 'magical', 'dramatic', 'intense', 'shadowy', 'forbidding', 'enchanted'];
|
|
102
|
+
const lowerText = text.toLowerCase();
|
|
103
|
+
if (boringWords.some(word => lowerText.includes(word))) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (interestingWords.some(word => lowerText.includes(word))) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return text.split(' ').length > 8 && text.length > 60;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* RPG Event Generator - Procedural event generation for RPG games
|
|
115
|
+
* @class
|
|
116
|
+
*/
|
|
117
|
+
class RPGEventGenerator {
|
|
118
|
+
/**
|
|
119
|
+
* Create a new event generator
|
|
120
|
+
* @param {Object} options - Configuration options
|
|
121
|
+
* @param {number} options.stateSize - Markov chain state size (default: 2)
|
|
122
|
+
* @param {Array} options.trainingData - Custom training data for Markov chains
|
|
123
|
+
*/
|
|
124
|
+
constructor(options = {}) {
|
|
125
|
+
this.chance = new Chance();
|
|
126
|
+
this.markovGenerator = new SimpleMarkovGenerator({
|
|
127
|
+
stateSize: options.stateSize || 2
|
|
128
|
+
});
|
|
129
|
+
const defaultTrainingData = options.trainingData || [
|
|
130
|
+
// Noble Court Intrigue
|
|
131
|
+
'The royal court is abuzz with whispers of scandal and betrayal', 'A noble lord approaches you with a proposition that could change your destiny', 'Court politics have reached a fever pitch as alliances shift like desert sands', 'The king\'s advisors plot in shadowed corners while the court dances obliviously', 'A mysterious letter arrives sealed with wax from a noble house you don\'t recognize',
|
|
132
|
+
// Criminal Underworld
|
|
133
|
+
'The thieves\' guild has put out a contract that bears your name', 'Shadowy figures lurk in alleyways, watching your every move', 'The black market thrives under the cover of night, offering forbidden luxuries', 'A notorious crime lord has taken an interest in your activities', 'Corrupt guards demand tribute while turning a blind eye to greater crimes',
|
|
134
|
+
// Supernatural & Mysterious
|
|
135
|
+
'Strange runes appear on your bedroom wall, glowing with ethereal light', 'An ancient prophecy speaks of a hero who matches your description exactly', 'Ghostly apparitions warn of impending doom in fevered dreams', 'A witch in the woods offers you power beyond mortal comprehension', 'Cursed artifacts surface in the market, promising great power at terrible cost',
|
|
136
|
+
// Personal & Dramatic
|
|
137
|
+
'Your past sins come back to haunt you in the most unexpected ways', 'A long-lost relative appears with a tale that shakes your world', 'Your reputation draws admirers and enemies in equal measure', 'A betrayal cuts deeper than any blade, leaving scars on your soul', 'Love and ambition war within your heart as opportunities arise',
|
|
138
|
+
// Adventure & Exploration
|
|
139
|
+
'Ancient ruins whisper secrets to those brave enough to listen', 'A dragon\'s hoard lies hidden, protected by trials and tribulations', 'Bandits rule the roads, but their leader seems oddly familiar', 'A legendary artifact calls to you from across distant lands', 'The wilderness holds both peril and promise for the bold adventurer',
|
|
140
|
+
// Social & Relationship
|
|
141
|
+
'Romantic entanglements complicate your carefully laid plans', 'Old friends become new enemies as loyalties are tested', 'Family secrets emerge that threaten to destroy everything you hold dear', 'Mentors offer wisdom that comes with strings attached', 'Rivals emerge from unexpected places, challenging your hard-won position',
|
|
142
|
+
// Economic & Mercantile
|
|
143
|
+
'The market crashes send shockwaves through the merchant class', 'A get-rich-quick scheme promises fortunes but demands your soul', 'Trade wars erupt between rival merchant houses', 'Investment opportunities arise that could make or break your fortune', 'Black market deals offer power but carry the weight of damnation',
|
|
144
|
+
// Military & Combat
|
|
145
|
+
'War drums beat as kingdoms prepare for inevitable conflict', 'Desertion offers freedom but brands you a coward forever', 'A duel of honor is proposed, with your reputation on the line', 'Mercenary companies seek captains brave enough to lead them', 'Battle scars tell stories of glory and horror in equal measure',
|
|
146
|
+
// Magical & Mystical
|
|
147
|
+
'The veil between worlds thins, allowing magic to seep into reality', 'Curses and blessings intertwine in ways you never expected', 'Ancient bloodlines awaken powers dormant for generations', 'Rituals performed in secret grant power at terrible personal cost', 'The stars themselves seem to conspire in your favor or against you',
|
|
148
|
+
// Seasonal & Natural
|
|
149
|
+
'Winter\'s cruel bite forces desperate measures from the populace', 'Spring\'s renewal brings both hope and dangerous new beginnings', 'Summer tournaments test the mettle of warriors and nobles alike', 'Autumn harvests reveal secrets long buried in the fields', 'The changing seasons mirror the turmoil in your own life',
|
|
150
|
+
// Dramatic Twists
|
|
151
|
+
'What seems like a blessing reveals itself as a curse in disguise', 'Enemies become allies, and allies become your greatest threat', 'The path of righteousness leads to ruin, while villainy brings reward', 'Fate itself seems to take notice of your actions, for good or ill', 'The world bends around you as your choices reshape reality itself'];
|
|
152
|
+
try {
|
|
153
|
+
this.markovGenerator.addData(defaultTrainingData);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.warn('Markov generator training failed:', error.message);
|
|
156
|
+
}
|
|
157
|
+
this.templates = {
|
|
158
|
+
COURT_SCANDAL: {
|
|
159
|
+
title: 'Court Scandal',
|
|
160
|
+
narrative: 'Whispers of infidelity and betrayal rock the royal court, threatening to expose secrets that could topple kingdoms.',
|
|
161
|
+
choices: [{
|
|
162
|
+
text: 'Exploit the scandal for personal gain',
|
|
163
|
+
effect: {
|
|
164
|
+
influence: [15, 30],
|
|
165
|
+
reputation: [-10, -5],
|
|
166
|
+
karma: [-10, -5]
|
|
167
|
+
},
|
|
168
|
+
consequence: 'court_manipulator'
|
|
169
|
+
}, {
|
|
170
|
+
text: 'Help contain the scandal',
|
|
171
|
+
effect: {
|
|
172
|
+
influence: [5, 15],
|
|
173
|
+
reputation: [10, 20],
|
|
174
|
+
favor: 'royal'
|
|
175
|
+
},
|
|
176
|
+
consequence: 'court_ally'
|
|
177
|
+
}, {
|
|
178
|
+
text: 'Remain neutral and observe',
|
|
179
|
+
effect: {
|
|
180
|
+
influence: [0, 5]
|
|
181
|
+
},
|
|
182
|
+
consequence: 'court_observer'
|
|
183
|
+
}]
|
|
184
|
+
},
|
|
185
|
+
NOBLE_DUEL: {
|
|
186
|
+
title: 'Challenge of Honor',
|
|
187
|
+
narrative: 'A noble of considerable standing has insulted your honor and demands satisfaction through single combat.',
|
|
188
|
+
choices: [{
|
|
189
|
+
text: 'Accept the duel immediately',
|
|
190
|
+
effect: {
|
|
191
|
+
reputation: [20, 40],
|
|
192
|
+
health: [-20, -5]
|
|
193
|
+
},
|
|
194
|
+
requirements: {
|
|
195
|
+
combat_skill: 50
|
|
196
|
+
},
|
|
197
|
+
consequence: 'duelist'
|
|
198
|
+
}, {
|
|
199
|
+
text: 'Demand a champion fight in your stead',
|
|
200
|
+
effect: {
|
|
201
|
+
gold: [-200, -100],
|
|
202
|
+
reputation: [5, 15]
|
|
203
|
+
},
|
|
204
|
+
consequence: 'strategist'
|
|
205
|
+
}, {
|
|
206
|
+
text: 'Apologize and offer compensation',
|
|
207
|
+
effect: {
|
|
208
|
+
gold: [-500, -200],
|
|
209
|
+
reputation: [-15, -5]
|
|
210
|
+
},
|
|
211
|
+
consequence: 'diplomat'
|
|
212
|
+
}]
|
|
213
|
+
},
|
|
214
|
+
THIEVES_GUILD: {
|
|
215
|
+
title: 'Shadows Call',
|
|
216
|
+
narrative: 'The thieves\' guild has noticed your growing influence and extends an invitation to join their ranks.',
|
|
217
|
+
choices: [{
|
|
218
|
+
text: 'Accept their offer',
|
|
219
|
+
effect: {
|
|
220
|
+
gold: [100, 300],
|
|
221
|
+
underworld_contacts: true
|
|
222
|
+
},
|
|
223
|
+
consequence: 'guild_member'
|
|
224
|
+
}, {
|
|
225
|
+
text: 'Negotiate better terms',
|
|
226
|
+
effect: {
|
|
227
|
+
influence: [10, 25],
|
|
228
|
+
negotiation_success: [0.6, 0.9]
|
|
229
|
+
},
|
|
230
|
+
consequence: 'guild_negotiator'
|
|
231
|
+
}, {
|
|
232
|
+
text: 'Reject them firmly',
|
|
233
|
+
effect: {
|
|
234
|
+
reputation: [5, 15],
|
|
235
|
+
underworld_enemies: true
|
|
236
|
+
},
|
|
237
|
+
consequence: 'guild_enemy'
|
|
238
|
+
}]
|
|
239
|
+
},
|
|
240
|
+
BLACKMAIL_OPPORTUNITY: {
|
|
241
|
+
title: 'Compromising Evidence',
|
|
242
|
+
narrative: 'You\'ve discovered evidence that could ruin a powerful figure. How you handle this will define your character.',
|
|
243
|
+
choices: [{
|
|
244
|
+
text: 'Use it for blackmail',
|
|
245
|
+
effect: {
|
|
246
|
+
gold: [500, 1500],
|
|
247
|
+
reputation: [-20, -10],
|
|
248
|
+
karma: [-20, -10]
|
|
249
|
+
},
|
|
250
|
+
consequence: 'blackmailer'
|
|
251
|
+
}, {
|
|
252
|
+
text: 'Destroy the evidence',
|
|
253
|
+
effect: {
|
|
254
|
+
reputation: [15, 30],
|
|
255
|
+
karma: [10, 20]
|
|
256
|
+
},
|
|
257
|
+
consequence: 'honorable'
|
|
258
|
+
}, {
|
|
259
|
+
text: 'Sell it to the highest bidder',
|
|
260
|
+
effect: {
|
|
261
|
+
gold: [200, 800],
|
|
262
|
+
underworld_contacts: true
|
|
263
|
+
},
|
|
264
|
+
consequence: 'information_broker'
|
|
265
|
+
}]
|
|
266
|
+
},
|
|
267
|
+
ANCIENT_CURSE: {
|
|
268
|
+
title: 'Ancient Curse Awakens',
|
|
269
|
+
narrative: 'An ancient artifact you touched has awakened a curse that now plagues your dreams and waking hours.',
|
|
270
|
+
choices: [{
|
|
271
|
+
text: 'Seek a priest\'s blessing',
|
|
272
|
+
effect: {
|
|
273
|
+
gold: [-100, -50],
|
|
274
|
+
health: [10, 25],
|
|
275
|
+
faith: [10, 20]
|
|
276
|
+
},
|
|
277
|
+
consequence: 'blessed'
|
|
278
|
+
}, {
|
|
279
|
+
text: 'Consult a witch or sorcerer',
|
|
280
|
+
effect: {
|
|
281
|
+
gold: [-300, -100],
|
|
282
|
+
curse_lifted: [0.4, 0.7]
|
|
283
|
+
},
|
|
284
|
+
consequence: 'occultist'
|
|
285
|
+
}, {
|
|
286
|
+
text: 'Endure the curse\'s effects',
|
|
287
|
+
effect: {
|
|
288
|
+
health: [-15, -5],
|
|
289
|
+
stress: [10, 25]
|
|
290
|
+
},
|
|
291
|
+
consequence: 'cursed'
|
|
292
|
+
}]
|
|
293
|
+
},
|
|
294
|
+
GHOSTLY_VISITATION: {
|
|
295
|
+
title: 'Spirit from the Past',
|
|
296
|
+
narrative: 'The ghost of someone from your past appears, demanding justice or offering redemption.',
|
|
297
|
+
choices: [{
|
|
298
|
+
text: 'Help the spirit find peace',
|
|
299
|
+
effect: {
|
|
300
|
+
karma: [15, 30],
|
|
301
|
+
stress: [5, 15]
|
|
302
|
+
},
|
|
303
|
+
consequence: 'redeemer'
|
|
304
|
+
}, {
|
|
305
|
+
text: 'Demand answers from the spirit',
|
|
306
|
+
effect: {
|
|
307
|
+
knowledge: [10, 25],
|
|
308
|
+
stress: [10, 20]
|
|
309
|
+
},
|
|
310
|
+
consequence: 'truth_seeker'
|
|
311
|
+
}, {
|
|
312
|
+
text: 'Banish the spirit forcefully',
|
|
313
|
+
effect: {
|
|
314
|
+
karma: [-10, -5],
|
|
315
|
+
stress: [15, 25]
|
|
316
|
+
},
|
|
317
|
+
consequence: 'spirit_banisher'
|
|
318
|
+
}]
|
|
319
|
+
},
|
|
320
|
+
FORBIDDEN_LOVE: {
|
|
321
|
+
title: 'Forbidden Romance',
|
|
322
|
+
narrative: 'Your heart has been captured by someone utterly unsuitable - a married noble, a sworn enemy, or someone from a forbidden class.',
|
|
323
|
+
choices: [{
|
|
324
|
+
text: 'Pursue the romance secretly',
|
|
325
|
+
effect: {
|
|
326
|
+
happiness: [20, 40],
|
|
327
|
+
stress: [10, 25],
|
|
328
|
+
scandal_risk: [0.3, 0.7]
|
|
329
|
+
},
|
|
330
|
+
consequence: 'secret_lover'
|
|
331
|
+
}, {
|
|
332
|
+
text: 'End it before it begins',
|
|
333
|
+
effect: {
|
|
334
|
+
happiness: [-10, -5],
|
|
335
|
+
reputation: [5, 10]
|
|
336
|
+
},
|
|
337
|
+
consequence: 'disciplined'
|
|
338
|
+
}, {
|
|
339
|
+
text: 'Confess everything publicly',
|
|
340
|
+
effect: {
|
|
341
|
+
reputation: [-30, -10],
|
|
342
|
+
influence: [-15, -5],
|
|
343
|
+
drama: true
|
|
344
|
+
},
|
|
345
|
+
consequence: 'dramatic'
|
|
346
|
+
}]
|
|
347
|
+
},
|
|
348
|
+
FAMILY_SECRET: {
|
|
349
|
+
title: 'Family Secret Revealed',
|
|
350
|
+
narrative: 'A shocking family secret emerges that could destroy your lineage or elevate it to legendary status.',
|
|
351
|
+
choices: [{
|
|
352
|
+
text: 'Embrace the secret and use it',
|
|
353
|
+
effect: {
|
|
354
|
+
influence: [20, 40],
|
|
355
|
+
family_legacy: true
|
|
356
|
+
},
|
|
357
|
+
consequence: 'ambitious'
|
|
358
|
+
}, {
|
|
359
|
+
text: 'Hide the secret at all costs',
|
|
360
|
+
effect: {
|
|
361
|
+
gold: [-500, -200],
|
|
362
|
+
stress: [15, 30]
|
|
363
|
+
},
|
|
364
|
+
consequence: 'protector'
|
|
365
|
+
}, {
|
|
366
|
+
text: 'Reveal it to the world',
|
|
367
|
+
effect: {
|
|
368
|
+
reputation: [-25, 5],
|
|
369
|
+
influence: [10, 30]
|
|
370
|
+
},
|
|
371
|
+
consequence: 'truthful'
|
|
372
|
+
}]
|
|
373
|
+
},
|
|
374
|
+
LOST_CIVILIZATION: {
|
|
375
|
+
title: 'Lost Civilization Discovered',
|
|
376
|
+
narrative: 'Your explorations have led to the ruins of an ancient civilization, filled with treasures and dangers.',
|
|
377
|
+
choices: [{
|
|
378
|
+
text: 'Loot everything you can carry',
|
|
379
|
+
effect: {
|
|
380
|
+
gold: [1000, 3000],
|
|
381
|
+
health: [-10, -30],
|
|
382
|
+
cursed_item: [0.2, 0.5]
|
|
383
|
+
},
|
|
384
|
+
consequence: 'treasure_hunter'
|
|
385
|
+
}, {
|
|
386
|
+
text: 'Study the artifacts carefully',
|
|
387
|
+
effect: {
|
|
388
|
+
knowledge: [20, 40],
|
|
389
|
+
influence: [10, 25]
|
|
390
|
+
},
|
|
391
|
+
consequence: 'archaeologist'
|
|
392
|
+
}, {
|
|
393
|
+
text: 'Leave the site undisturbed',
|
|
394
|
+
effect: {
|
|
395
|
+
karma: [10, 20],
|
|
396
|
+
reputation: [5, 15]
|
|
397
|
+
},
|
|
398
|
+
consequence: 'preserver'
|
|
399
|
+
}]
|
|
400
|
+
},
|
|
401
|
+
BANDIT_KING: {
|
|
402
|
+
title: 'Bandit King\'s Challenge',
|
|
403
|
+
narrative: 'The infamous bandit king blocks your path, offering you a choice: join his band or face certain death.',
|
|
404
|
+
choices: [{
|
|
405
|
+
text: 'Join the bandits',
|
|
406
|
+
effect: {
|
|
407
|
+
gold: [200, 500],
|
|
408
|
+
reputation: [-30, -15],
|
|
409
|
+
combat_skill: [5, 15]
|
|
410
|
+
},
|
|
411
|
+
consequence: 'bandit'
|
|
412
|
+
}, {
|
|
413
|
+
text: 'Challenge him to single combat',
|
|
414
|
+
effect: {
|
|
415
|
+
reputation: [20, 40],
|
|
416
|
+
health: [-30, -10]
|
|
417
|
+
},
|
|
418
|
+
requirements: {
|
|
419
|
+
combat_skill: 60
|
|
420
|
+
},
|
|
421
|
+
consequence: 'hero'
|
|
422
|
+
}, {
|
|
423
|
+
text: 'Bribe your way past',
|
|
424
|
+
effect: {
|
|
425
|
+
gold: [-400, -200],
|
|
426
|
+
safe_passage: true
|
|
427
|
+
},
|
|
428
|
+
consequence: 'diplomat'
|
|
429
|
+
}]
|
|
430
|
+
},
|
|
431
|
+
MARKET_CRASH: {
|
|
432
|
+
title: 'Market Catastrophe',
|
|
433
|
+
narrative: 'A sudden market crash threatens to wipe out fortunes across the kingdom, including your own investments.',
|
|
434
|
+
choices: [{
|
|
435
|
+
text: 'Sell everything immediately',
|
|
436
|
+
effect: {
|
|
437
|
+
gold: [-30, -10],
|
|
438
|
+
market_recovery: true
|
|
439
|
+
},
|
|
440
|
+
consequence: 'cautious_trader'
|
|
441
|
+
}, {
|
|
442
|
+
text: 'Buy up distressed assets',
|
|
443
|
+
effect: {
|
|
444
|
+
gold: [-1000, 2000],
|
|
445
|
+
risk: [0.4, 0.8]
|
|
446
|
+
},
|
|
447
|
+
consequence: 'speculator'
|
|
448
|
+
}, {
|
|
449
|
+
text: 'Hold and weather the storm',
|
|
450
|
+
effect: {
|
|
451
|
+
gold: [-50, 50],
|
|
452
|
+
reputation: [10, 20]
|
|
453
|
+
},
|
|
454
|
+
consequence: 'patient_investor'
|
|
455
|
+
}]
|
|
456
|
+
},
|
|
457
|
+
TRADE_WAR: {
|
|
458
|
+
title: 'Trade War Escalates',
|
|
459
|
+
narrative: 'Rival merchant houses have declared economic warfare, and you\'re caught in the middle.',
|
|
460
|
+
choices: [{
|
|
461
|
+
text: 'Form an alliance with one side',
|
|
462
|
+
effect: {
|
|
463
|
+
influence: [15, 30],
|
|
464
|
+
gold: [200, 600]
|
|
465
|
+
},
|
|
466
|
+
consequence: 'alliance_builder'
|
|
467
|
+
}, {
|
|
468
|
+
text: 'Play both sides against each other',
|
|
469
|
+
effect: {
|
|
470
|
+
gold: [500, 1500],
|
|
471
|
+
reputation: [-15, -5]
|
|
472
|
+
},
|
|
473
|
+
consequence: 'manipulator'
|
|
474
|
+
}, {
|
|
475
|
+
text: 'Stay neutral and wait it out',
|
|
476
|
+
effect: {
|
|
477
|
+
gold: [-100, 0],
|
|
478
|
+
reputation: [5, 15]
|
|
479
|
+
},
|
|
480
|
+
consequence: 'neutral_party'
|
|
481
|
+
}]
|
|
482
|
+
},
|
|
483
|
+
DESERTION_TEMPTATION: {
|
|
484
|
+
title: 'Cowardice or Wisdom?',
|
|
485
|
+
narrative: 'Your commanding officer leads your unit into certain slaughter. Desertion offers survival, but at the cost of honor.',
|
|
486
|
+
choices: [{
|
|
487
|
+
text: 'Desert and save yourself',
|
|
488
|
+
effect: {
|
|
489
|
+
health: [0, 10],
|
|
490
|
+
reputation: [-40, -20],
|
|
491
|
+
stress: [20, 40]
|
|
492
|
+
},
|
|
493
|
+
consequence: 'deserter'
|
|
494
|
+
}, {
|
|
495
|
+
text: 'Lead a mutiny against the commander',
|
|
496
|
+
effect: {
|
|
497
|
+
influence: [30, 60],
|
|
498
|
+
reputation: [-20, 10]
|
|
499
|
+
},
|
|
500
|
+
consequence: 'mutineer'
|
|
501
|
+
}, {
|
|
502
|
+
text: 'Stand and fight with honor',
|
|
503
|
+
effect: {
|
|
504
|
+
reputation: [20, 40],
|
|
505
|
+
health: [-40, -20]
|
|
506
|
+
},
|
|
507
|
+
consequence: 'hero'
|
|
508
|
+
}]
|
|
509
|
+
},
|
|
510
|
+
MERCENARY_CONTRACT: {
|
|
511
|
+
title: 'Lucrative but Dangerous',
|
|
512
|
+
narrative: 'A wealthy patron offers you a mercenary contract that promises great wealth but involves fighting against impossible odds.',
|
|
513
|
+
choices: [{
|
|
514
|
+
text: 'Accept the contract',
|
|
515
|
+
effect: {
|
|
516
|
+
gold: [1000, 3000],
|
|
517
|
+
health: [-30, -10],
|
|
518
|
+
reputation: [10, 30]
|
|
519
|
+
},
|
|
520
|
+
consequence: 'mercenary'
|
|
521
|
+
}, {
|
|
522
|
+
text: 'Negotiate better terms',
|
|
523
|
+
effect: {
|
|
524
|
+
gold: [800, 2000],
|
|
525
|
+
influence: [10, 25]
|
|
526
|
+
},
|
|
527
|
+
consequence: 'negotiator'
|
|
528
|
+
}, {
|
|
529
|
+
text: 'Decline the offer',
|
|
530
|
+
effect: {
|
|
531
|
+
reputation: [5, 15]
|
|
532
|
+
},
|
|
533
|
+
consequence: 'prudent'
|
|
534
|
+
}]
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Generate a rich, contextual event based on player state
|
|
541
|
+
* @param {Object} playerContext - Player stats and state
|
|
542
|
+
* @returns {Object} Generated event with deep narrative
|
|
543
|
+
*/
|
|
544
|
+
generateEvent(playerContext = {}) {
|
|
545
|
+
const context = this.analyzeContext(playerContext);
|
|
546
|
+
const template = this.selectTemplate(context);
|
|
547
|
+
return {
|
|
548
|
+
id: `event_${Date.now()}_${this.chance.guid().substring(0, 8)}`,
|
|
549
|
+
title: this.generateDynamicTitle(template, context),
|
|
550
|
+
description: this.generateRichDescription(template, context),
|
|
551
|
+
narrative: template.narrative,
|
|
552
|
+
choices: this.generateContextualChoices(template.choices, context),
|
|
553
|
+
type: Object.keys(this.templates).find(key => this.templates[key] === template),
|
|
554
|
+
consequence: null,
|
|
555
|
+
context: context,
|
|
556
|
+
urgency: this.calculateUrgency(template, context),
|
|
557
|
+
theme: this.determineTheme(template, context)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Generate multiple events at once
|
|
563
|
+
* @param {Object} playerContext - Player stats and state
|
|
564
|
+
* @param {number} count - Number of events to generate
|
|
565
|
+
* @returns {Array} Array of generated events
|
|
566
|
+
*/
|
|
567
|
+
generateEvents(playerContext = {}, count = 1) {
|
|
568
|
+
const events = [];
|
|
569
|
+
for (let i = 0; i < count; i++) {
|
|
570
|
+
events.push(this.generateEvent(playerContext));
|
|
571
|
+
}
|
|
572
|
+
return events;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Analyze and enrich player context for sophisticated event generation
|
|
577
|
+
* @private
|
|
578
|
+
*/
|
|
579
|
+
analyzeContext(playerContext) {
|
|
580
|
+
// Handle null or undefined playerContext
|
|
581
|
+
const ctx = playerContext || {};
|
|
582
|
+
const context = {
|
|
583
|
+
age: ctx.age || 16,
|
|
584
|
+
wealth: ctx.gold || 0,
|
|
585
|
+
influence: ctx.influence || 0,
|
|
586
|
+
reputation: ctx.reputation || 0,
|
|
587
|
+
career: ctx.career || null,
|
|
588
|
+
skills: ctx.skills || {},
|
|
589
|
+
relationships: ctx.relationships || [],
|
|
590
|
+
location: ctx.location || this.generateLocation(),
|
|
591
|
+
season: ctx.season || 'spring',
|
|
592
|
+
health: ctx.health || 100,
|
|
593
|
+
stress: ctx.stress || 0,
|
|
594
|
+
happiness: ctx.happiness || 50,
|
|
595
|
+
karma: ctx.karma || 0,
|
|
596
|
+
faith: ctx.faith || 0,
|
|
597
|
+
vices: ctx.vices || [],
|
|
598
|
+
secrets: ctx.secrets || [],
|
|
599
|
+
ambitions: ctx.ambitions || []
|
|
600
|
+
};
|
|
601
|
+
context.social_standing = this.calculateSocialStanding(context);
|
|
602
|
+
context.power_level = this.calculatePowerLevel(context);
|
|
603
|
+
context.life_experience = this.calculateLifeExperience(context);
|
|
604
|
+
return context;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Calculate social standing based on various factors
|
|
609
|
+
* @private
|
|
610
|
+
*/
|
|
611
|
+
calculateSocialStanding(context) {
|
|
612
|
+
let standing = 0;
|
|
613
|
+
standing += context.influence * 0.4;
|
|
614
|
+
standing += context.reputation * 0.3;
|
|
615
|
+
standing += context.wealth / 100 * 0.3;
|
|
616
|
+
if (context.career && context.career.toLowerCase().includes('noble')) {
|
|
617
|
+
standing += 20;
|
|
618
|
+
}
|
|
619
|
+
return Math.max(0, Math.min(100, standing));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Calculate overall power level
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
calculatePowerLevel(context) {
|
|
627
|
+
let power = 0;
|
|
628
|
+
power += context.influence * 0.3;
|
|
629
|
+
power += context.wealth / 100 * 0.2;
|
|
630
|
+
power += Object.values(context.skills).reduce((sum, skill) => sum + skill, 0) / 10 * 0.2;
|
|
631
|
+
power += context.relationships.length * 5 * 0.1;
|
|
632
|
+
power += (100 - context.stress) * 0.2;
|
|
633
|
+
return Math.max(0, power);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Calculate life experience factor
|
|
638
|
+
* @private
|
|
639
|
+
*/
|
|
640
|
+
calculateLifeExperience(context) {
|
|
641
|
+
let experience = context.age * 0.5;
|
|
642
|
+
experience += Object.values(context.skills).reduce((sum, skill) => sum + skill, 0) * 0.1;
|
|
643
|
+
experience += context.relationships.length * 2;
|
|
644
|
+
experience += context.ambitions.length * 3;
|
|
645
|
+
return experience;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Generate a random location for context
|
|
650
|
+
* @private
|
|
651
|
+
*/
|
|
652
|
+
generateLocation() {
|
|
653
|
+
const locations = ['capital', 'border town', 'coastal city', 'mountain village', 'forest outpost', 'desert caravan stop', 'riverside settlement', 'island fortress', 'underground city', 'floating market'];
|
|
654
|
+
return this.chance.pickone(locations);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Select the most appropriate template based on rich context analysis
|
|
659
|
+
* @private
|
|
660
|
+
*/
|
|
661
|
+
selectTemplate(context) {
|
|
662
|
+
const templateKeys = Object.keys(this.templates);
|
|
663
|
+
const weights = this.calculateTemplateWeights(context, templateKeys);
|
|
664
|
+
const selectedKey = this.chance.weighted(templateKeys, weights);
|
|
665
|
+
return this.templates[selectedKey];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Calculate dynamic weights for template selection based on context
|
|
670
|
+
* @private
|
|
671
|
+
*/
|
|
672
|
+
calculateTemplateWeights(context, templateKeys) {
|
|
673
|
+
const baseWeights = {};
|
|
674
|
+
templateKeys.forEach(key => {
|
|
675
|
+
const template = this.templates[key];
|
|
676
|
+
baseWeights[key] = this.getBaseWeightForTemplate(key, context);
|
|
677
|
+
});
|
|
678
|
+
this.applyContextModifiers(baseWeights, context);
|
|
679
|
+
const totalWeight = Object.values(baseWeights).reduce((sum, w) => sum + w, 0);
|
|
680
|
+
const normalizedWeights = Object.values(baseWeights).map(w => w / totalWeight);
|
|
681
|
+
return normalizedWeights;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Get base weight for a template type
|
|
686
|
+
* @private
|
|
687
|
+
*/
|
|
688
|
+
getBaseWeightForTemplate(templateKey, context) {
|
|
689
|
+
const weights = {
|
|
690
|
+
COURT_SCANDAL: 0.08,
|
|
691
|
+
NOBLE_DUEL: 0.06,
|
|
692
|
+
THIEVES_GUILD: 0.07,
|
|
693
|
+
BLACKMAIL_OPPORTUNITY: 0.05,
|
|
694
|
+
ANCIENT_CURSE: 0.04,
|
|
695
|
+
GHOSTLY_VISITATION: 0.03,
|
|
696
|
+
FORBIDDEN_LOVE: 0.06,
|
|
697
|
+
FAMILY_SECRET: 0.04,
|
|
698
|
+
LOST_CIVILIZATION: 0.05,
|
|
699
|
+
BANDIT_KING: 0.07,
|
|
700
|
+
MARKET_CRASH: 0.06,
|
|
701
|
+
TRADE_WAR: 0.05,
|
|
702
|
+
DESERTION_TEMPTATION: 0.03,
|
|
703
|
+
MERCENARY_CONTRACT: 0.04
|
|
704
|
+
};
|
|
705
|
+
return weights[templateKey] || 0.05;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Apply context-based modifiers to template weights
|
|
710
|
+
* @private
|
|
711
|
+
*/
|
|
712
|
+
applyContextModifiers(weights, context) {
|
|
713
|
+
if (context.career) {
|
|
714
|
+
const career = context.career.toLowerCase();
|
|
715
|
+
if (career.includes('noble') || career.includes('court')) {
|
|
716
|
+
weights.COURT_SCANDAL *= 2.5;
|
|
717
|
+
weights.NOBLE_DUEL *= 2.0;
|
|
718
|
+
weights.FORBIDDEN_LOVE *= 1.8;
|
|
719
|
+
}
|
|
720
|
+
if (career.includes('merchant') || career.includes('trade')) {
|
|
721
|
+
weights.MARKET_CRASH *= 2.2;
|
|
722
|
+
weights.TRADE_WAR *= 2.0;
|
|
723
|
+
weights.BLACKMAIL_OPPORTUNITY *= 1.5;
|
|
724
|
+
}
|
|
725
|
+
if (career.includes('thief') || career.includes('criminal')) {
|
|
726
|
+
weights.THIEVES_GUILD *= 2.5;
|
|
727
|
+
weights.BLACKMAIL_OPPORTUNITY *= 2.0;
|
|
728
|
+
}
|
|
729
|
+
if (career.includes('warrior') || career.includes('knight')) {
|
|
730
|
+
weights.NOBLE_DUEL *= 2.2;
|
|
731
|
+
weights.MERCENARY_CONTRACT *= 1.8;
|
|
732
|
+
weights.DESERTION_TEMPTATION *= 1.5;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Age-based modifiers
|
|
737
|
+
if (context.age < 25) {
|
|
738
|
+
weights.FORBIDDEN_LOVE *= 1.6;
|
|
739
|
+
weights.FAMILY_SECRET *= 1.4;
|
|
740
|
+
} else if (context.age > 50) {
|
|
741
|
+
weights.GHOSTLY_VISITATION *= 1.8;
|
|
742
|
+
weights.FAMILY_SECRET *= 1.6;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Wealth-based modifiers
|
|
746
|
+
if (context.wealth > 1000) {
|
|
747
|
+
weights.MARKET_CRASH *= 1.8;
|
|
748
|
+
weights.TRADE_WAR *= 1.6;
|
|
749
|
+
weights.BLACKMAIL_OPPORTUNITY *= 1.4;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Influence-based modifiers
|
|
753
|
+
if (context.influence > 50) {
|
|
754
|
+
weights.COURT_SCANDAL *= 2.0;
|
|
755
|
+
weights.NOBLE_DUEL *= 1.8;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Skill-based modifiers
|
|
759
|
+
if (context.skills.combat > 60) {
|
|
760
|
+
weights.NOBLE_DUEL *= 1.6;
|
|
761
|
+
weights.MERCENARY_CONTRACT *= 1.4;
|
|
762
|
+
}
|
|
763
|
+
if (context.skills.diplomacy > 60) {
|
|
764
|
+
weights.COURT_SCANDAL *= 1.5;
|
|
765
|
+
weights.FORBIDDEN_LOVE *= 1.3;
|
|
766
|
+
}
|
|
767
|
+
if (context.skills.thievery > 60) {
|
|
768
|
+
weights.THIEVES_GUILD *= 1.7;
|
|
769
|
+
weights.BLACKMAIL_OPPORTUNITY *= 1.5;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Reputation-based modifiers
|
|
773
|
+
if (context.reputation < -20) {
|
|
774
|
+
weights.THIEVES_GUILD *= 1.8;
|
|
775
|
+
weights.BLACKMAIL_OPPORTUNITY *= 2.0;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Relationship-based modifiers
|
|
779
|
+
if (context.relationships && context.relationships.length > 3) {
|
|
780
|
+
weights.FORBIDDEN_LOVE *= 1.4;
|
|
781
|
+
weights.FAMILY_SECRET *= 1.6;
|
|
782
|
+
weights.COURT_SCANDAL *= 1.3;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Seasonal modifiers
|
|
786
|
+
if (context.season === 'winter') {
|
|
787
|
+
weights.GHOSTLY_VISITATION *= 1.5;
|
|
788
|
+
weights.ANCIENT_CURSE *= 1.3;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Generate dynamic, context-aware title
|
|
794
|
+
* @private
|
|
795
|
+
*/
|
|
796
|
+
generateDynamicTitle(template, context) {
|
|
797
|
+
const titleModifiers = {
|
|
798
|
+
dramatic: ['Shocking', 'Unbelievable', 'Extraordinary', 'Monumental'],
|
|
799
|
+
urgent: ['Critical', 'Immediate', 'Pressing', 'Urgent'],
|
|
800
|
+
mysterious: ['Enigmatic', 'Puzzling', 'Curious', 'Intriguing'],
|
|
801
|
+
personal: ['Intimate', 'Personal', 'Private', 'Close'],
|
|
802
|
+
dangerous: ['Perilous', 'Dangerous', 'Risky', 'Treacherous']
|
|
803
|
+
};
|
|
804
|
+
let modifierType = 'dramatic';
|
|
805
|
+
const templateKey = Object.keys(this.templates).find(key => this.templates[key] === template);
|
|
806
|
+
if (templateKey.includes('SCANDAL') || templateKey.includes('DUEL') || templateKey.includes('CRASH')) {
|
|
807
|
+
modifierType = 'urgent';
|
|
808
|
+
} else if (templateKey.includes('CURSE') || templateKey.includes('GHOST') || templateKey.includes('SECRET')) {
|
|
809
|
+
modifierType = 'mysterious';
|
|
810
|
+
} else if (templateKey.includes('LOVE') || templateKey.includes('FAMILY')) {
|
|
811
|
+
modifierType = 'personal';
|
|
812
|
+
} else if (templateKey.includes('BANDIT') || templateKey.includes('DESERTION') || templateKey.includes('MERCENARY')) {
|
|
813
|
+
modifierType = 'dangerous';
|
|
814
|
+
}
|
|
815
|
+
const modifier = this.chance.pickone(titleModifiers[modifierType]);
|
|
816
|
+
return `${modifier} ${template.title}`;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Generate rich, contextual description with personality
|
|
821
|
+
* @private
|
|
822
|
+
*/
|
|
823
|
+
generateRichDescription(template, context) {
|
|
824
|
+
try {
|
|
825
|
+
let description = template.narrative;
|
|
826
|
+
const generated = this.markovGenerator.generate({
|
|
827
|
+
minLength: 30,
|
|
828
|
+
maxLength: 80,
|
|
829
|
+
allowDuplicates: false,
|
|
830
|
+
maxTries: 5
|
|
831
|
+
});
|
|
832
|
+
if (generated && generated.string && generated.string.length > 40 && generated.string.split(' ').length > 6 && !this.data.some(training => training === generated.string) && this.isInterestingText(generated.string)) {
|
|
833
|
+
description = generated.string + '. ' + description.charAt(0).toLowerCase() + description.slice(1);
|
|
834
|
+
}
|
|
835
|
+
const contextAdditions = this.generateContextAdditions(template, context);
|
|
836
|
+
if (contextAdditions.length > 0) {
|
|
837
|
+
description += ' ' + this.chance.pickone(contextAdditions);
|
|
838
|
+
}
|
|
839
|
+
if (this.chance.bool({
|
|
840
|
+
likelihood: 40
|
|
841
|
+
})) {
|
|
842
|
+
const consequenceHint = this.generateConsequenceHint(template, context);
|
|
843
|
+
if (consequenceHint) {
|
|
844
|
+
description += ' ' + consequenceHint;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return description;
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return template.narrative;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Generate contextual additions to descriptions
|
|
855
|
+
* @private
|
|
856
|
+
*/
|
|
857
|
+
generateContextAdditions(template, context) {
|
|
858
|
+
const additions = [];
|
|
859
|
+
if (context.career) {
|
|
860
|
+
const career = context.career.toLowerCase();
|
|
861
|
+
if (career.includes('noble') || career.includes('court')) {
|
|
862
|
+
additions.push('Your position in court makes this matter particularly sensitive.');
|
|
863
|
+
additions.push('The royal family\'s involvement complicates everything.');
|
|
864
|
+
}
|
|
865
|
+
if (career.includes('merchant')) {
|
|
866
|
+
additions.push('Your business interests are directly affected by this development.');
|
|
867
|
+
additions.push('The merchant guilds are already positioning themselves.');
|
|
868
|
+
}
|
|
869
|
+
if (career.includes('warrior') || career.includes('knight')) {
|
|
870
|
+
additions.push('Your martial prowess could turn the tide of this situation.');
|
|
871
|
+
additions.push('The battlefield calls, and honor demands a response.');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Age-based additions
|
|
876
|
+
if (context.age < 25) {
|
|
877
|
+
additions.push('At your young age, this experience could shape your entire future.');
|
|
878
|
+
} else if (context.age > 60) {
|
|
879
|
+
additions.push('With age comes wisdom, but also the weight of past decisions.');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Relationship-based additions
|
|
883
|
+
if (context.relationships && context.relationships.length > 0) {
|
|
884
|
+
additions.push('Your personal connections may influence how this unfolds.');
|
|
885
|
+
additions.push('Someone you know is intimately involved in this matter.');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Seasonal additions
|
|
889
|
+
if (context.season) {
|
|
890
|
+
const seasonAdditions = {
|
|
891
|
+
winter: 'The harsh winter weather makes resolution all the more urgent.',
|
|
892
|
+
spring: 'Spring\'s renewal brings hope, but also unpredictable change.',
|
|
893
|
+
summer: 'Summer\'s heat mirrors the intensity of the situation.',
|
|
894
|
+
autumn: 'Autumn\'s harvest season reminds us that all things must end.'
|
|
895
|
+
};
|
|
896
|
+
if (seasonAdditions[context.season]) {
|
|
897
|
+
additions.push(seasonAdditions[context.season]);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return additions;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Generate subtle hints about potential consequences
|
|
905
|
+
* @private
|
|
906
|
+
*/
|
|
907
|
+
generateConsequenceHint(template, context) {
|
|
908
|
+
const hints = ['The choices you make here will echo through your future.', 'This decision carries weight beyond what you can immediately see.', 'Your response will define how others perceive you.', 'The consequences of this moment will be felt for years to come.', 'Choose wisely, for the wrong path leads to ruin.', 'This crossroads will determine your destiny.'];
|
|
909
|
+
const hintChance = Math.min(0.4, (context.influence + Math.abs(context.reputation)) / 200);
|
|
910
|
+
if (this.chance.bool({
|
|
911
|
+
likelihood: hintChance * 100
|
|
912
|
+
})) {
|
|
913
|
+
return this.chance.pickone(hints);
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Generate choices that adapt to player context and have meaningful consequences
|
|
920
|
+
* @private
|
|
921
|
+
*/
|
|
922
|
+
generateContextualChoices(templateChoices, context) {
|
|
923
|
+
return templateChoices.map(choice => {
|
|
924
|
+
const resolvedChoice = {
|
|
925
|
+
...choice
|
|
926
|
+
};
|
|
927
|
+
resolvedChoice.effect = this.resolveEffects(choice.effect, context);
|
|
928
|
+
resolvedChoice.text = this.enhanceChoiceText(choice.text, context);
|
|
929
|
+
if (choice.consequence) {
|
|
930
|
+
resolvedChoice.consequence = choice.consequence;
|
|
931
|
+
}
|
|
932
|
+
return resolvedChoice;
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Resolve effect ranges to specific values based on context
|
|
938
|
+
* @private
|
|
939
|
+
*/
|
|
940
|
+
resolveEffects(effectRanges, context) {
|
|
941
|
+
const resolved = {};
|
|
942
|
+
Object.entries(effectRanges).forEach(([key, value]) => {
|
|
943
|
+
if (Array.isArray(value)) {
|
|
944
|
+
let min = value[0];
|
|
945
|
+
let max = value[1];
|
|
946
|
+
const multiplier = this.getContextMultiplier(key, context);
|
|
947
|
+
min = Math.round(min * multiplier);
|
|
948
|
+
max = Math.round(max * multiplier);
|
|
949
|
+
if (min > max) {
|
|
950
|
+
[min, max] = [max, min];
|
|
951
|
+
}
|
|
952
|
+
resolved[key] = this.chance.integer({
|
|
953
|
+
min,
|
|
954
|
+
max
|
|
955
|
+
});
|
|
956
|
+
} else if (typeof value === 'number') {
|
|
957
|
+
resolved[key] = value;
|
|
958
|
+
} else {
|
|
959
|
+
resolved[key] = value;
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
return resolved;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Get context multiplier for effects
|
|
967
|
+
* @private
|
|
968
|
+
*/
|
|
969
|
+
getContextMultiplier(effectKey, context) {
|
|
970
|
+
let multiplier = 1.0;
|
|
971
|
+
switch (effectKey) {
|
|
972
|
+
case 'influence':
|
|
973
|
+
case 'reputation':
|
|
974
|
+
if (context.influence > 30) multiplier *= 1.2;
|
|
975
|
+
if (context.reputation > 20) multiplier *= 1.1;
|
|
976
|
+
break;
|
|
977
|
+
case 'gold':
|
|
978
|
+
if (context.wealth > 500) multiplier *= 1.3;
|
|
979
|
+
if (context.wealth < 100) multiplier *= 0.8;
|
|
980
|
+
break;
|
|
981
|
+
case 'health':
|
|
982
|
+
break;
|
|
983
|
+
case 'stress':
|
|
984
|
+
case 'happiness':
|
|
985
|
+
if (context.age > 40) multiplier *= 1.1; // More emotional impact with age
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
return multiplier;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Enhance choice text with contextual details
|
|
993
|
+
* @private
|
|
994
|
+
*/
|
|
995
|
+
enhanceChoiceText(baseText, context) {
|
|
996
|
+
const enhancements = [];
|
|
997
|
+
if (context.career && this.chance.bool({
|
|
998
|
+
likelihood: 20
|
|
999
|
+
})) {
|
|
1000
|
+
const career = context.career.toLowerCase();
|
|
1001
|
+
if (career.includes('noble')) {
|
|
1002
|
+
enhancements.push(' (with noble dignity)');
|
|
1003
|
+
} else if (career.includes('merchant')) {
|
|
1004
|
+
enhancements.push(' (calculating the profits)');
|
|
1005
|
+
} else if (career.includes('warrior')) {
|
|
1006
|
+
enhancements.push(' (with martial resolve)');
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (enhancements.length > 0) {
|
|
1010
|
+
return baseText + this.chance.pickone(enhancements);
|
|
1011
|
+
}
|
|
1012
|
+
return baseText;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Calculate event urgency based on template and context
|
|
1017
|
+
* @private
|
|
1018
|
+
*/
|
|
1019
|
+
calculateUrgency(template, context) {
|
|
1020
|
+
let urgency = 'normal';
|
|
1021
|
+
if (template.title.toLowerCase().includes('critical') || template.title.toLowerCase().includes('urgent') || template.narrative.toLowerCase().includes('immediately')) {
|
|
1022
|
+
urgency = 'high';
|
|
1023
|
+
}
|
|
1024
|
+
if (context.health < 30 || context.wealth < 50) {
|
|
1025
|
+
urgency = 'high';
|
|
1026
|
+
}
|
|
1027
|
+
if (context.influence > 70) {
|
|
1028
|
+
urgency = 'high';
|
|
1029
|
+
}
|
|
1030
|
+
return urgency;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Determine the thematic category of the event
|
|
1035
|
+
* @private
|
|
1036
|
+
*/
|
|
1037
|
+
determineTheme(template, context) {
|
|
1038
|
+
const templateKey = Object.keys(this.templates).find(key => this.templates[key] === template);
|
|
1039
|
+
if (templateKey.includes('COURT') || templateKey.includes('NOBLE')) {
|
|
1040
|
+
return 'political';
|
|
1041
|
+
} else if (templateKey.includes('THIEF') || templateKey.includes('BLACKMAIL')) {
|
|
1042
|
+
return 'criminal';
|
|
1043
|
+
} else if (templateKey.includes('CURSE') || templateKey.includes('GHOST')) {
|
|
1044
|
+
return 'supernatural';
|
|
1045
|
+
} else if (templateKey.includes('LOVE') || templateKey.includes('FAMILY')) {
|
|
1046
|
+
return 'personal';
|
|
1047
|
+
} else if (templateKey.includes('CIVILIZATION') || templateKey.includes('BANDIT')) {
|
|
1048
|
+
return 'adventure';
|
|
1049
|
+
} else if (templateKey.includes('MARKET') || templateKey.includes('TRADE')) {
|
|
1050
|
+
return 'economic';
|
|
1051
|
+
} else if (templateKey.includes('DESERTION') || templateKey.includes('MERCENARY')) {
|
|
1052
|
+
return 'military';
|
|
1053
|
+
}
|
|
1054
|
+
return 'general';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Generate contextual choices with resolved effects
|
|
1059
|
+
* @private
|
|
1060
|
+
*/
|
|
1061
|
+
generateChoices(baseChoices, context) {
|
|
1062
|
+
return baseChoices.map(choice => ({
|
|
1063
|
+
...choice,
|
|
1064
|
+
effect: this.resolveEffect(choice.effect, context)
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Resolve dynamic effects (ranges become specific numbers)
|
|
1070
|
+
* @private
|
|
1071
|
+
*/
|
|
1072
|
+
resolveEffect(effect, context) {
|
|
1073
|
+
const resolved = {};
|
|
1074
|
+
Object.entries(effect).forEach(([key, value]) => {
|
|
1075
|
+
if (Array.isArray(value)) {
|
|
1076
|
+
resolved[key] = this.chance.integer({
|
|
1077
|
+
min: value[0],
|
|
1078
|
+
max: value[1]
|
|
1079
|
+
});
|
|
1080
|
+
} else {
|
|
1081
|
+
resolved[key] = value;
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
if (context.wealth >= 1000 && resolved.gold > 0) {
|
|
1085
|
+
resolved.gold = Math.floor(resolved.gold * 1.5);
|
|
1086
|
+
}
|
|
1087
|
+
return resolved;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Generate a random location
|
|
1092
|
+
* @private
|
|
1093
|
+
*/
|
|
1094
|
+
generateLocation() {
|
|
1095
|
+
const locations = ['village', 'town', 'city', 'castle', 'forest', 'mountains', 'coast', 'desert'];
|
|
1096
|
+
return this.chance.pickone(locations);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Get fallback description if Markov generation fails
|
|
1101
|
+
* @private
|
|
1102
|
+
*/
|
|
1103
|
+
getFallbackDescription(templateType) {
|
|
1104
|
+
const fallbacks = {
|
|
1105
|
+
ENCOUNTER: `A ${faker.person.jobTitle()} approaches you with an unusual proposition.`,
|
|
1106
|
+
OPPORTUNITY: `A rare ${faker.commerce.product()} has become available at an exceptional price.`,
|
|
1107
|
+
CHALLENGE: `A ${faker.person.jobTitle()} challenges your position in the community.`,
|
|
1108
|
+
MYSTERY: `Strange occurrences have been reported near ${faker.location.city()}.`
|
|
1109
|
+
};
|
|
1110
|
+
return fallbacks[templateType] || fallbacks.ENCOUNTER;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Add custom training data for Markov generation
|
|
1115
|
+
* @param {Array} data - Array of strings to train on
|
|
1116
|
+
*/
|
|
1117
|
+
addTrainingData(data) {
|
|
1118
|
+
try {
|
|
1119
|
+
this.markovGenerator.addData(data);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
console.warn('Failed to add training data:', error.message);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Reset the Markov generator with new data
|
|
1127
|
+
* @param {Array} data - Array of strings to train on
|
|
1128
|
+
*/
|
|
1129
|
+
resetTrainingData(data) {
|
|
1130
|
+
this.markovGenerator = new SimpleMarkovGenerator({
|
|
1131
|
+
stateSize: 2
|
|
1132
|
+
});
|
|
1133
|
+
this.addTrainingData(data);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
exports.RPGEventGenerator = RPGEventGenerator;
|
|
1137
|
+
function generateRPGEvent(playerContext = {}) {
|
|
1138
|
+
const generator = new RPGEventGenerator();
|
|
1139
|
+
return generator.generateEvent(playerContext);
|
|
1140
|
+
}
|
|
1141
|
+
module.exports = {
|
|
1142
|
+
RPGEventGenerator: RPGEventGenerator,
|
|
1143
|
+
generateRPGEvent: generateRPGEvent
|
|
1144
|
+
};
|