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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +430 -0
  3. package/dist/index.js +1144 -0
  4. 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
+ };