rpg-event-generator 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -6
- package/dist/index.js +1482 -54
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -120,35 +120,56 @@ class RPGEventGenerator {
|
|
|
120
120
|
* @param {Object} options - Configuration options
|
|
121
121
|
* @param {number} options.stateSize - Markov chain state size (default: 2)
|
|
122
122
|
* @param {Array} options.trainingData - Custom training data for Markov chains
|
|
123
|
+
* @param {string} options.theme - Theme for event generation: 'fantasy', 'sci-fi', 'historical'
|
|
124
|
+
* @param {string} options.culture - Cultural context within theme (optional)
|
|
123
125
|
*/
|
|
124
126
|
constructor(options = {}) {
|
|
125
127
|
this.chance = new Chance();
|
|
126
128
|
this.markovGenerator = new SimpleMarkovGenerator({
|
|
127
129
|
stateSize: options.stateSize || 2
|
|
128
130
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
131
|
+
this.theme = options.theme || 'fantasy';
|
|
132
|
+
this.culture = options.culture;
|
|
133
|
+
this.customTemplates = new Set();
|
|
134
|
+
this.customChains = new Set();
|
|
135
|
+
this.customTrainingData = {};
|
|
136
|
+
this.initializeThemes();
|
|
137
|
+
this.activeChains = new Map();
|
|
138
|
+
this.chainDefinitions = this.initializeChainDefinitions();
|
|
139
|
+
this.difficultySettings = {
|
|
140
|
+
easy: {
|
|
141
|
+
powerRange: [0, 50],
|
|
142
|
+
rewardMultiplier: 1.5,
|
|
143
|
+
penaltyMultiplier: 0.7
|
|
144
|
+
},
|
|
145
|
+
normal: {
|
|
146
|
+
powerRange: [25, 150],
|
|
147
|
+
rewardMultiplier: 1.0,
|
|
148
|
+
penaltyMultiplier: 1.0
|
|
149
|
+
},
|
|
150
|
+
hard: {
|
|
151
|
+
powerRange: [100, 300],
|
|
152
|
+
rewardMultiplier: 0.8,
|
|
153
|
+
penaltyMultiplier: 1.3
|
|
154
|
+
},
|
|
155
|
+
legendary: {
|
|
156
|
+
powerRange: [250, 1000],
|
|
157
|
+
rewardMultiplier: 0.6,
|
|
158
|
+
penaltyMultiplier: 1.6
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Initialize time system
|
|
163
|
+
this.timeSystem = {
|
|
164
|
+
currentDay: 1,
|
|
165
|
+
currentSeason: 'spring',
|
|
166
|
+
gameYear: 1,
|
|
167
|
+
timeBasedEvents: new Map()
|
|
168
|
+
};
|
|
169
|
+
const selectedTheme = options.theme || 'fantasy';
|
|
170
|
+
const selectedCulture = options.culture;
|
|
171
|
+
const themeTrainingData = this.getThemeTrainingData(selectedTheme, selectedCulture);
|
|
172
|
+
const defaultTrainingData = options.trainingData || themeTrainingData;
|
|
152
173
|
try {
|
|
153
174
|
this.markovGenerator.addData(defaultTrainingData);
|
|
154
175
|
} catch (error) {
|
|
@@ -532,6 +553,370 @@ class RPGEventGenerator {
|
|
|
532
553
|
},
|
|
533
554
|
consequence: 'prudent'
|
|
534
555
|
}]
|
|
556
|
+
},
|
|
557
|
+
// Event Chain Templates
|
|
558
|
+
BANDIT_AMBUSH: {
|
|
559
|
+
title: 'Bandit Ambush',
|
|
560
|
+
narrative: 'A small group of bandits springs an ambush on your caravan, demanding tribute or blood.',
|
|
561
|
+
choices: [{
|
|
562
|
+
text: 'Fight back fiercely',
|
|
563
|
+
effect: {
|
|
564
|
+
health: [-20, -5],
|
|
565
|
+
reputation: [10, 25]
|
|
566
|
+
},
|
|
567
|
+
consequence: 'hero'
|
|
568
|
+
}, {
|
|
569
|
+
text: 'Pay the tribute',
|
|
570
|
+
effect: {
|
|
571
|
+
gold: [-200, -50],
|
|
572
|
+
reputation: [-10, -5]
|
|
573
|
+
},
|
|
574
|
+
consequence: 'prudent'
|
|
575
|
+
}, {
|
|
576
|
+
text: 'Join the bandits',
|
|
577
|
+
effect: {
|
|
578
|
+
reputation: [-20, -10],
|
|
579
|
+
combat_skill: [5, 10]
|
|
580
|
+
},
|
|
581
|
+
consequence: 'bandit'
|
|
582
|
+
}]
|
|
583
|
+
},
|
|
584
|
+
BANDIT_ARMY: {
|
|
585
|
+
title: 'Bandit Army Assembles',
|
|
586
|
+
narrative: 'The bandit king has gathered a formidable army. Villages are burning, and the threat grows daily.',
|
|
587
|
+
choices: [{
|
|
588
|
+
text: 'Lead a militia to confront them',
|
|
589
|
+
effect: {
|
|
590
|
+
influence: [20, 40],
|
|
591
|
+
health: [-40, -20]
|
|
592
|
+
},
|
|
593
|
+
consequence: 'leader'
|
|
594
|
+
}, {
|
|
595
|
+
text: 'Negotiate with the bandit king',
|
|
596
|
+
effect: {
|
|
597
|
+
gold: [-1000, -500],
|
|
598
|
+
reputation: [-5, 10]
|
|
599
|
+
},
|
|
600
|
+
consequence: 'diplomat'
|
|
601
|
+
}, {
|
|
602
|
+
text: 'Flee to safer lands',
|
|
603
|
+
effect: {
|
|
604
|
+
influence: [-15, -5],
|
|
605
|
+
stress: [-10, -5]
|
|
606
|
+
},
|
|
607
|
+
consequence: 'refugee'
|
|
608
|
+
}]
|
|
609
|
+
},
|
|
610
|
+
ASSASSINATION_PLOT: {
|
|
611
|
+
title: 'Assassination Plot Uncovered',
|
|
612
|
+
narrative: 'Your investigation has revealed a plot to assassinate a high-ranking noble. The conspirators know you\'re onto them.',
|
|
613
|
+
choices: [{
|
|
614
|
+
text: 'Alert the authorities immediately',
|
|
615
|
+
effect: {
|
|
616
|
+
influence: [15, 30],
|
|
617
|
+
reputation: [10, 20]
|
|
618
|
+
},
|
|
619
|
+
consequence: 'hero'
|
|
620
|
+
}, {
|
|
621
|
+
text: 'Confront the conspirators yourself',
|
|
622
|
+
effect: {
|
|
623
|
+
health: [-30, -10],
|
|
624
|
+
reputation: [20, 35]
|
|
625
|
+
},
|
|
626
|
+
consequence: 'investigator'
|
|
627
|
+
}, {
|
|
628
|
+
text: 'Use this information for personal gain',
|
|
629
|
+
effect: {
|
|
630
|
+
gold: [500, 1500],
|
|
631
|
+
reputation: [-25, -10]
|
|
632
|
+
},
|
|
633
|
+
consequence: 'blackmailer'
|
|
634
|
+
}]
|
|
635
|
+
},
|
|
636
|
+
ROYAL_PURGE: {
|
|
637
|
+
title: 'Royal Purge',
|
|
638
|
+
narrative: 'The scandal has triggered a royal purge. Accusations fly, and no one is safe from suspicion.',
|
|
639
|
+
choices: [{
|
|
640
|
+
text: 'Stand your ground and proclaim innocence',
|
|
641
|
+
effect: {
|
|
642
|
+
reputation: [15, 30],
|
|
643
|
+
influence: [10, 25]
|
|
644
|
+
},
|
|
645
|
+
consequence: 'innocent'
|
|
646
|
+
}, {
|
|
647
|
+
text: 'Flee into exile',
|
|
648
|
+
effect: {
|
|
649
|
+
influence: [-50, -20],
|
|
650
|
+
stress: [10, 25]
|
|
651
|
+
},
|
|
652
|
+
consequence: 'exile'
|
|
653
|
+
}, {
|
|
654
|
+
text: 'Testify against others to save yourself',
|
|
655
|
+
effect: {
|
|
656
|
+
reputation: [-30, -15],
|
|
657
|
+
karma: [-20, -10]
|
|
658
|
+
},
|
|
659
|
+
consequence: 'betrayer'
|
|
660
|
+
}]
|
|
661
|
+
},
|
|
662
|
+
TRADE_OPPORTUNITY: {
|
|
663
|
+
title: 'Rare Trade Opportunity',
|
|
664
|
+
narrative: 'A lucrative trade route has opened up, but it requires significant initial investment.',
|
|
665
|
+
choices: [{
|
|
666
|
+
text: 'Invest heavily in the venture',
|
|
667
|
+
effect: {
|
|
668
|
+
gold: [-2000, -1000],
|
|
669
|
+
influence: [10, 25]
|
|
670
|
+
},
|
|
671
|
+
consequence: 'invest'
|
|
672
|
+
}, {
|
|
673
|
+
text: 'Take a calculated risk with moderate investment',
|
|
674
|
+
effect: {
|
|
675
|
+
gold: [-500, -200],
|
|
676
|
+
influence: [5, 15]
|
|
677
|
+
},
|
|
678
|
+
consequence: 'moderate'
|
|
679
|
+
}, {
|
|
680
|
+
text: 'Pass on the opportunity',
|
|
681
|
+
effect: {
|
|
682
|
+
reputation: [-5, 0]
|
|
683
|
+
},
|
|
684
|
+
consequence: 'cautious'
|
|
685
|
+
}]
|
|
686
|
+
},
|
|
687
|
+
FINAL_RECKONING: {
|
|
688
|
+
title: 'Final Reckoning',
|
|
689
|
+
narrative: 'The cursed artifact demands its final price. The entity bound to it appears before you.',
|
|
690
|
+
choices: [{
|
|
691
|
+
text: 'Destroy the artifact',
|
|
692
|
+
effect: {
|
|
693
|
+
health: [-50, -20],
|
|
694
|
+
karma: [30, 50]
|
|
695
|
+
},
|
|
696
|
+
consequence: 'destroyer'
|
|
697
|
+
}, {
|
|
698
|
+
text: 'Bargain with the entity',
|
|
699
|
+
effect: {
|
|
700
|
+
influence: [20, 40],
|
|
701
|
+
health: [-20, -10]
|
|
702
|
+
},
|
|
703
|
+
consequence: 'diplomat'
|
|
704
|
+
}, {
|
|
705
|
+
text: 'Accept the curse as your own',
|
|
706
|
+
effect: {
|
|
707
|
+
power_level: [50, 100],
|
|
708
|
+
health: [-30, -15]
|
|
709
|
+
},
|
|
710
|
+
consequence: 'cursed'
|
|
711
|
+
}]
|
|
712
|
+
},
|
|
713
|
+
// Seasonal Event Templates
|
|
714
|
+
BLOOMING_ROMANCE: {
|
|
715
|
+
title: 'Spring Blossoms',
|
|
716
|
+
narrative: 'The spring air carries the sweet scent of blooming flowers, and romance fills the atmosphere.',
|
|
717
|
+
choices: [{
|
|
718
|
+
text: 'Pursue a romantic interest',
|
|
719
|
+
effect: {
|
|
720
|
+
happiness: [20, 40],
|
|
721
|
+
relationships: [1, 2]
|
|
722
|
+
},
|
|
723
|
+
consequence: 'romantic'
|
|
724
|
+
}, {
|
|
725
|
+
text: 'Focus on personal growth',
|
|
726
|
+
effect: {
|
|
727
|
+
wisdom: [10, 25],
|
|
728
|
+
stress: [-10, -5]
|
|
729
|
+
},
|
|
730
|
+
consequence: 'introspective'
|
|
731
|
+
}, {
|
|
732
|
+
text: 'Throw a garden party',
|
|
733
|
+
effect: {
|
|
734
|
+
influence: [10, 20],
|
|
735
|
+
gold: [-50, -20]
|
|
736
|
+
},
|
|
737
|
+
consequence: 'social'
|
|
738
|
+
}]
|
|
739
|
+
},
|
|
740
|
+
SPRING_FESTIVAL: {
|
|
741
|
+
title: 'Festival of Renewal',
|
|
742
|
+
narrative: 'The village celebrates the spring festival with flowers, music, and joyous gatherings.',
|
|
743
|
+
choices: [{
|
|
744
|
+
text: 'Participate in the celebrations',
|
|
745
|
+
effect: {
|
|
746
|
+
happiness: [15, 30],
|
|
747
|
+
reputation: [5, 15]
|
|
748
|
+
},
|
|
749
|
+
consequence: 'festive'
|
|
750
|
+
}, {
|
|
751
|
+
text: 'Help organize the festival',
|
|
752
|
+
effect: {
|
|
753
|
+
influence: [15, 25],
|
|
754
|
+
gold: [-30, -10]
|
|
755
|
+
},
|
|
756
|
+
consequence: 'organizer'
|
|
757
|
+
}, {
|
|
758
|
+
text: 'Use the crowds for business',
|
|
759
|
+
effect: {
|
|
760
|
+
gold: [20, 50],
|
|
761
|
+
reputation: [-5, 5]
|
|
762
|
+
},
|
|
763
|
+
consequence: 'merchant'
|
|
764
|
+
}]
|
|
765
|
+
},
|
|
766
|
+
SUMMER_TOURNAMENT: {
|
|
767
|
+
title: 'Grand Tournament',
|
|
768
|
+
narrative: 'Warriors from across the land gather for the annual summer tournament, testing their skills in combat.',
|
|
769
|
+
choices: [{
|
|
770
|
+
text: 'Enter the tournament',
|
|
771
|
+
effect: {
|
|
772
|
+
reputation: [20, 40],
|
|
773
|
+
health: [-20, -5]
|
|
774
|
+
},
|
|
775
|
+
consequence: 'competitor'
|
|
776
|
+
}, {
|
|
777
|
+
text: 'Sponsor a contestant',
|
|
778
|
+
effect: {
|
|
779
|
+
influence: [10, 20],
|
|
780
|
+
gold: [-100, -50]
|
|
781
|
+
},
|
|
782
|
+
consequence: 'patron'
|
|
783
|
+
}, {
|
|
784
|
+
text: 'Watch and learn',
|
|
785
|
+
effect: {
|
|
786
|
+
combat_skill: [5, 15],
|
|
787
|
+
reputation: [5, 10]
|
|
788
|
+
},
|
|
789
|
+
consequence: 'spectator'
|
|
790
|
+
}]
|
|
791
|
+
},
|
|
792
|
+
HARVEST_FESTIVAL: {
|
|
793
|
+
title: 'Harvest Celebration',
|
|
794
|
+
narrative: 'The autumn harvest brings bounty and gratitude, celebrated with feasts and thanksgiving.',
|
|
795
|
+
choices: [{
|
|
796
|
+
text: 'Contribute to the harvest',
|
|
797
|
+
effect: {
|
|
798
|
+
karma: [10, 20],
|
|
799
|
+
community_relations: [15, 25]
|
|
800
|
+
},
|
|
801
|
+
consequence: 'contributor'
|
|
802
|
+
}, {
|
|
803
|
+
text: 'Host a harvest feast',
|
|
804
|
+
effect: {
|
|
805
|
+
influence: [20, 30],
|
|
806
|
+
gold: [-80, -40]
|
|
807
|
+
},
|
|
808
|
+
consequence: 'host'
|
|
809
|
+
}, {
|
|
810
|
+
text: 'Trade harvest goods',
|
|
811
|
+
effect: {
|
|
812
|
+
gold: [50, 100],
|
|
813
|
+
influence: [5, 15]
|
|
814
|
+
},
|
|
815
|
+
consequence: 'trader'
|
|
816
|
+
}]
|
|
817
|
+
},
|
|
818
|
+
WINTER_SOLSTICE: {
|
|
819
|
+
title: 'Winter Solstice Ritual',
|
|
820
|
+
narrative: 'The longest night brings reflection and hope as the sun begins its return journey.',
|
|
821
|
+
choices: [{
|
|
822
|
+
text: 'Participate in solstice rituals',
|
|
823
|
+
effect: {
|
|
824
|
+
faith: [10, 25],
|
|
825
|
+
stress: [-15, -5]
|
|
826
|
+
},
|
|
827
|
+
consequence: 'spiritual'
|
|
828
|
+
}, {
|
|
829
|
+
text: 'Host a solstice gathering',
|
|
830
|
+
effect: {
|
|
831
|
+
influence: [15, 25],
|
|
832
|
+
warmth: [20, 40]
|
|
833
|
+
},
|
|
834
|
+
consequence: 'community'
|
|
835
|
+
}, {
|
|
836
|
+
text: 'Contemplate the turning year',
|
|
837
|
+
effect: {
|
|
838
|
+
wisdom: [15, 30],
|
|
839
|
+
insight: [10, 20]
|
|
840
|
+
},
|
|
841
|
+
consequence: 'philosophical'
|
|
842
|
+
}]
|
|
843
|
+
},
|
|
844
|
+
// Time-based Event Templates
|
|
845
|
+
RUMORS_OF_DISSENT: {
|
|
846
|
+
title: 'Whispers of Unrest',
|
|
847
|
+
narrative: 'Rumors of dissatisfaction with the current regime begin circulating through the streets.',
|
|
848
|
+
choices: [{
|
|
849
|
+
text: 'Investigate the rumors',
|
|
850
|
+
effect: {
|
|
851
|
+
influence: [5, 15],
|
|
852
|
+
risk: [10, 20]
|
|
853
|
+
},
|
|
854
|
+
consequence: 'investigator'
|
|
855
|
+
}, {
|
|
856
|
+
text: 'Ignore the gossip',
|
|
857
|
+
effect: {
|
|
858
|
+
stress: [-5, 0]
|
|
859
|
+
},
|
|
860
|
+
consequence: 'apathetic'
|
|
861
|
+
}, {
|
|
862
|
+
text: 'Spread counter-rumors',
|
|
863
|
+
effect: {
|
|
864
|
+
reputation: [-10, -5],
|
|
865
|
+
influence: [5, 10]
|
|
866
|
+
},
|
|
867
|
+
consequence: 'loyalist'
|
|
868
|
+
}]
|
|
869
|
+
},
|
|
870
|
+
MARKET_UNREST: {
|
|
871
|
+
title: 'Market Instability',
|
|
872
|
+
narrative: 'Traders report unusual fluctuations in market prices and growing uncertainty among merchants.',
|
|
873
|
+
choices: [{
|
|
874
|
+
text: 'Invest in stable goods',
|
|
875
|
+
effect: {
|
|
876
|
+
gold: [-200, -100],
|
|
877
|
+
security: [20, 40]
|
|
878
|
+
},
|
|
879
|
+
consequence: 'conservative'
|
|
880
|
+
}, {
|
|
881
|
+
text: 'Speculate on market changes',
|
|
882
|
+
effect: {
|
|
883
|
+
gold: [-100, 300],
|
|
884
|
+
risk: [30, 50]
|
|
885
|
+
},
|
|
886
|
+
consequence: 'speculator'
|
|
887
|
+
}, {
|
|
888
|
+
text: 'Diversify your holdings',
|
|
889
|
+
effect: {
|
|
890
|
+
gold: [-50, -20],
|
|
891
|
+
stability: [15, 25]
|
|
892
|
+
},
|
|
893
|
+
consequence: 'balanced'
|
|
894
|
+
}]
|
|
895
|
+
},
|
|
896
|
+
STRANGE_OCCURRENCES: {
|
|
897
|
+
title: 'Unexplained Phenomena',
|
|
898
|
+
narrative: 'Strange lights in the sky and unexplained events begin occurring with increasing frequency.',
|
|
899
|
+
choices: [{
|
|
900
|
+
text: 'Investigate the phenomena',
|
|
901
|
+
effect: {
|
|
902
|
+
knowledge: [10, 25],
|
|
903
|
+
risk: [10, 20]
|
|
904
|
+
},
|
|
905
|
+
consequence: 'curious'
|
|
906
|
+
}, {
|
|
907
|
+
text: 'Consult local experts',
|
|
908
|
+
effect: {
|
|
909
|
+
gold: [-30, -10],
|
|
910
|
+
insight: [15, 30]
|
|
911
|
+
},
|
|
912
|
+
consequence: 'scholarly'
|
|
913
|
+
}, {
|
|
914
|
+
text: 'Dismiss as superstition',
|
|
915
|
+
effect: {
|
|
916
|
+
stress: [-10, -5]
|
|
917
|
+
},
|
|
918
|
+
consequence: 'skeptical'
|
|
919
|
+
}]
|
|
535
920
|
}
|
|
536
921
|
};
|
|
537
922
|
}
|
|
@@ -544,17 +929,19 @@ class RPGEventGenerator {
|
|
|
544
929
|
generateEvent(playerContext = {}) {
|
|
545
930
|
const context = this.analyzeContext(playerContext);
|
|
546
931
|
const template = this.selectTemplate(context);
|
|
932
|
+
const scaledChoices = this.scaleEffectsForDifficulty(this.generateContextualChoices(template.choices, context), context);
|
|
547
933
|
return {
|
|
548
934
|
id: `event_${Date.now()}_${this.chance.guid().substring(0, 8)}`,
|
|
549
935
|
title: this.generateDynamicTitle(template, context),
|
|
550
936
|
description: this.generateRichDescription(template, context),
|
|
551
937
|
narrative: template.narrative,
|
|
552
|
-
choices:
|
|
938
|
+
choices: scaledChoices,
|
|
553
939
|
type: Object.keys(this.templates).find(key => this.templates[key] === template),
|
|
554
940
|
consequence: null,
|
|
555
941
|
context: context,
|
|
556
942
|
urgency: this.calculateUrgency(template, context),
|
|
557
|
-
theme: this.determineTheme(template, context)
|
|
943
|
+
theme: this.determineTheme(template, context),
|
|
944
|
+
difficulty: this.calculateDifficultyTier(context.power_level || 0)
|
|
558
945
|
};
|
|
559
946
|
}
|
|
560
947
|
|
|
@@ -573,39 +960,991 @@ class RPGEventGenerator {
|
|
|
573
960
|
}
|
|
574
961
|
|
|
575
962
|
/**
|
|
576
|
-
*
|
|
963
|
+
* Initialize thematic training data sets
|
|
577
964
|
* @private
|
|
578
965
|
*/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
966
|
+
initializeThemes() {
|
|
967
|
+
this.themes = {
|
|
968
|
+
fantasy: [
|
|
969
|
+
// Noble Court Intrigue
|
|
970
|
+
'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',
|
|
971
|
+
// Criminal Underworld
|
|
972
|
+
'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',
|
|
973
|
+
// Supernatural & Mysterious
|
|
974
|
+
'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',
|
|
975
|
+
// Personal & Dramatic
|
|
976
|
+
'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',
|
|
977
|
+
// Adventure & Exploration
|
|
978
|
+
'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',
|
|
979
|
+
// Social & Relationship
|
|
980
|
+
'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',
|
|
981
|
+
// Economic & Mercantile
|
|
982
|
+
'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',
|
|
983
|
+
// Military & Combat
|
|
984
|
+
'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',
|
|
985
|
+
// Magical & Mystical
|
|
986
|
+
'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',
|
|
987
|
+
// Seasonal & Natural
|
|
988
|
+
'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',
|
|
989
|
+
// Dramatic Twists
|
|
990
|
+
'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'],
|
|
991
|
+
'sci-fi': [
|
|
992
|
+
// Corporate Intrigue
|
|
993
|
+
'Megacorporations wage shadow wars through proxy conflicts and data manipulation', 'Neural implants malfunction, flooding your mind with corporate secrets', 'A black-market AI offers forbidden knowledge at the cost of your sanity', 'Corporate espionage turns deadly when rival agents converge on your location', 'Whistleblower data reveals systemic corruption in the highest levels of power',
|
|
994
|
+
// Cyberpunk Underworld
|
|
995
|
+
'Hackers breach your personal network, exposing vulnerabilities you never knew existed', 'Street gangs control the undercity with drone swarms and illegal augmentations', 'The dark web pulses with illegal tech trades and forbidden experiments', 'Corporate bounty hunters track you through the neon-lit sprawl', 'Data runners risk everything for the ultimate information score',
|
|
996
|
+
// Space & Exploration
|
|
997
|
+
'Alien artifacts whisper secrets from beyond the galactic rim', 'Space pirates demand tribute or face the void of decompression', 'Ancient derelict ships hold treasures and horrors from forgotten eras', 'Wormhole anomalies bend reality, creating unpredictable temporal events', 'Colony worlds rebel against Earth\'s iron-fisted governance',
|
|
998
|
+
// AI & Technology
|
|
999
|
+
'Sentient AIs manipulate events from within the global network', 'Nanobot swarms escape containment, evolving beyond their programming', 'Virtual reality simulations bleed into the real world disastrously', 'Genetic engineering experiments create monstrous hybrid beings', 'Quantum computers predict the future, but at what cost to free will?',
|
|
1000
|
+
// Dystopian Society
|
|
1001
|
+
'Social credit systems reward compliance and punish deviation', 'Surveillance drones watch every move, every transaction, every thought', 'Rebellions simmer beneath the surface of totalitarian control', 'Resource wars rage over the last viable mining asteroids', 'Climate engineering projects backfire spectacularly',
|
|
1002
|
+
// Personal & Cybernetic
|
|
1003
|
+
'Implant malfunctions cause vivid hallucinations and system crashes', 'Identity theft becomes literal as someone steals your digital consciousness', 'Memory wipes erase crucial knowledge, leaving you vulnerable', 'Cybernetic enhancements demand maintenance that costs more than you can afford', 'Neural links create unintended telepathic connections with strangers',
|
|
1004
|
+
// Economic & Corporate
|
|
1005
|
+
'Cryptocurrency markets crash, wiping out fortunes in digital dust', 'Hostile takeovers turn friendly acquisitions into bloody boardroom coups', 'Trade wars erupt between orbital habitats and planetary governments', 'Investment opportunities in black-market tech promise riches or ruin', 'Corporate espionage turns personal when family members are leveraged',
|
|
1006
|
+
// Military & Combat
|
|
1007
|
+
'Drone wars rage in the skies above abandoned cities', 'Cyber warfare disables entire infrastructure networks', 'Private military companies offer their services to the highest bidder', 'Nuclear deterrence fails when EMP weapons render missiles useless', 'Bioterrorism releases engineered plagues upon unsuspecting populations',
|
|
1008
|
+
// Mystery & Conspiracy
|
|
1009
|
+
'Government cover-ups hide the truth about alien visitations', 'Time travel experiments create paradoxical timeline fractures', 'Parallel dimensions bleed through, creating impossible phenomena', 'Ancient alien technology reactivates with catastrophic consequences', 'Conspiracy theories prove true, but the reality is far worse',
|
|
1010
|
+
// Environmental & Planetary
|
|
1011
|
+
'Terraforming projects unleash ancient microbes from the soil', 'Climate control systems fail, creating unpredictable weather patterns', 'Asteroid mining operations awaken dormant extraterrestrial threats', 'Ocean acidification drives aquatic mutations to the surface', 'Atmospheric processors malfunction, creating toxic breathing conditions'],
|
|
1012
|
+
historical: [
|
|
1013
|
+
// Medieval Court Intrigue
|
|
1014
|
+
'The king\'s advisors plot in shadowed corners of the great hall', 'Noble houses form secret alliances against the crown', 'Whispers of heresy spread through the royal court like wildfire', 'A foreign diplomat offers treacherous propositions under diplomatic immunity', 'Court jesters hide dangerous truths behind masks of folly',
|
|
1015
|
+
// Warfare & Conquest
|
|
1016
|
+
'Armies mass at the borders, their banners fluttering in the wind of war', 'Siege engines pound ancient walls as defenders hold the line', 'Cavalry charges break enemy formations in clouds of dust and blood', 'Naval blockades starve cities into submission or desperate rebellion', 'Mercenary companies switch sides based on the highest bidder',
|
|
1017
|
+
// Exploration & Discovery
|
|
1018
|
+
'New World expeditions return with gold and strange diseases', 'Ancient artifacts surface from archaeological digs, rewriting history', 'Trade caravans brave bandits and harsh terrains for exotic goods', 'Mapping expeditions chart unknown territories and hostile natives', 'Scientific discoveries challenge religious doctrines and established truths',
|
|
1019
|
+
// Social & Class Conflict
|
|
1020
|
+
'Peasant revolts spread like contagion through the countryside', 'Guilds clash over trade monopolies and apprenticeship rights', 'Religious schisms divide communities along ideological lines', 'Women maneuver within restrictive social structures for power and influence', 'Immigrants bring new customs that clash with traditional ways',
|
|
1021
|
+
// Economic & Mercantile
|
|
1022
|
+
'Market monopolies drive independent merchants to desperation', 'Banking houses manipulate currency values for political gain', 'Trade embargoes create black markets and smuggling operations', 'Agricultural failures lead to famines and social unrest', 'Colonial exploitation enriches the crown while impoverishing subjects',
|
|
1023
|
+
// Political & Diplomatic
|
|
1024
|
+
'Treaty negotiations mask underlying territorial ambitions', 'Spy networks weave webs of deception across national borders', 'Succession crises threaten to plunge nations into civil war', 'Diplomatic incidents escalate into full-scale military conflicts', 'Royal marriages forge alliances that crumble under stress',
|
|
1025
|
+
// Religious & Ideological
|
|
1026
|
+
'Inquisitions root out heresy with fire and torture', 'Crusades call warriors to distant lands for faith and glory', 'Reformation movements challenge the authority of established churches', 'Cult leaders promise salvation through devotion and sacrifice', 'Religious relics inspire pilgrimages and bloody conflicts',
|
|
1027
|
+
// Personal & Dramatic
|
|
1028
|
+
'Family feuds span generations, poisoning bloodlines with vengeance', 'Scandals rock aristocratic families, threatening social standing', 'Personal ambitions clash with familial duties and expectations', 'Love affairs defy social conventions and familial arrangements', 'Betrayals cut deeper than any blade, leaving lasting scars',
|
|
1029
|
+
// Natural & Environmental
|
|
1030
|
+
'Plagues sweep through densely packed cities, claiming thousands', 'Famines force desperate migrations and social breakdowns', 'Natural disasters reveal underlying social and political tensions', 'Harsh winters test the limits of human endurance and charity', 'Droughts drive conflicts over dwindling water resources',
|
|
1031
|
+
// Innovation & Progress
|
|
1032
|
+
'Scientific discoveries revolutionize warfare and exploration', 'Technological innovations disrupt traditional crafts and livelihoods', 'Medical breakthroughs save lives but challenge religious views', 'Educational reforms open knowledge to new social classes', 'Engineering marvels push the boundaries of human capability']
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// Cultural variants within themes
|
|
1036
|
+
this.cultures = {
|
|
1037
|
+
fantasy: {
|
|
1038
|
+
norse: ['The longships sail into the fjord under the northern lights', 'Runes carved in ancient stone whisper forgotten secrets', 'The mead hall echoes with tales of heroic deeds and tragic fates', 'Berserkers rage through the village leaving destruction in their wake', 'The gods themselves seem to take notice of mortal affairs', 'Viking raiders demand tribute or face the wrath of the axe', 'Shamanic visions reveal the threads of fate woven by the Norns', 'The sacred grove hides a portal to the realm of the Aesir', 'Blood oaths bind warriors to quests of honor and vengeance', 'Thunder rolls as Thor\'s hammer strikes down the unworthy'],
|
|
1039
|
+
arabian: ['The desert winds carry secrets from across the golden sands', 'The sultan\'s palace gleams with marble and precious gems', 'Caravans traverse ancient trade routes laden with exotic spices', 'Djinn emerge from bottles granting wishes with treacherous twists', 'The bazaar bustles with merchants hawking forbidden artifacts', 'Flying carpets soar above the minarets of ancient cities', 'Oasis springs hide treasures guarded by serpentine efreet', 'Bedouin tribes honor ancient codes of hospitality and revenge', 'The caliph\'s viziers plot intricate webs of palace intrigue', 'Magic lamps hold genies bound by oaths older than the desert'],
|
|
1040
|
+
celtic: ['The ancient stone circles hum with druidic power', 'Mists roll in from the moors carrying spectral visitors', 'The high king\'s court debates matters of honor and clan loyalty', 'Leprechauns guard pots of gold at the end of rainbows', 'Banshees wail warnings of impending doom from castle towers', 'The green hills hide fairy mounds and trooping fae', 'Druidic circles perform rituals beneath the full moon', 'Clan tartans flutter as warriors clash in honorable combat', 'The sidhe court offers dangerous bargains to mortal visitors', 'Standing stones mark portals to the otherworld'],
|
|
1041
|
+
asian: ['The imperial court buzzes with whispers of dynastic intrigue', 'Paper lanterns float on mist-shrouded temple lakes', 'The great wall stands as both protector and prison', 'Dragon spirits guard ancestral treasures in mountain monasteries', 'Tea ceremonies reveal secrets shared only with trusted allies', 'Samurai honor demands seppuku for failures of duty', 'The emperor\'s astronomers chart the movements of celestial dragons', 'Rice paddies hide the entrances to underworld spirit realms', 'Calligraphy brushes write prophecies on silk scrolls', 'The five elements balance or clash in matters of fate and fortune']
|
|
1042
|
+
},
|
|
1043
|
+
'sci-fi': {
|
|
1044
|
+
cyberpunk: ['Neon lights reflect off rain-slicked streets in the megacity sprawl', 'Corporate security forces patrol the undercity with neural implants', 'Black market clinics offer experimental augmentations at deadly prices', 'Data runners jack into the net, risking their minds for digital secrets', 'Street gangs control territory with drone swarms and illegal tech', 'Megacorporations wage proxy wars through shell companies and hackers', 'Neural lace interfaces blur the line between human and machine', 'The undergrid pulses with illegal transmissions and rebel broadcasts', 'Synthcoke dealers promise escape from the corporate grind', 'Rogue AIs manifest as ghosts in the machine, manipulating events'],
|
|
1045
|
+
space_opera: ['Star destroyers cast shadows across colonized planets', 'The galactic senate debates matters of interstellar diplomacy', 'Smugglers navigate asteroid fields laden with contraband cargo', 'Alien ambassadors negotiate treaties in crystalline council chambers', 'Hyperspace routes hide pirate ambushes and temporal anomalies', 'The emperor\'s new clothes are actually powered armor', 'Jedi knights wield lightsabers in duels of honor and destiny', 'Space stations orbit gas giants, hubs of trade and intrigue', 'The force guides the worthy through trials of wisdom and combat', 'Rebel alliances form in the shadows of imperial domination'],
|
|
1046
|
+
post_apocalyptic: ['Mutated creatures roam the irradiated wastelands', 'Vault dwellers emerge from underground bunkers into harsh sunlight', 'Caravan masters trade caps for water and ammunition', 'Super mutants patrol ruined highways in power armor', 'The brotherhood preserves technology as holy relics', 'Ghoul settlements offer sanctuary at the cost of sanity', 'Deathclaws stalk the ruins of pre-war cities', 'Enclave remnants cling to old world ideologies', 'Wasteland doctors perform surgery with scavenged medical supplies', 'Tribal shamans commune with the spirits of the old world']
|
|
1047
|
+
},
|
|
1048
|
+
historical: {
|
|
1049
|
+
medieval: ['Knights joust in tournament fields beneath colorful pennants', 'The castle keep stands as bastion against siege engines', 'Peasant revolts spread like wildfire through feudal lands', 'The black death claims lives across town and countryside', 'Alchemists in hidden laboratories seek the philosopher\'s stone', 'Crusader armies march under banners of faith and conquest', 'Courtly love complicates matters of honor and duty', 'The Magna Carta is sealed with wax and noble promises', 'Witch hunters burn heretics at public stake executions', 'Guild masters guard trade secrets with jealous vigilance'],
|
|
1050
|
+
victorian: ['Steam engines power the industrial revolution across soot-stained cities', 'The British Empire spans continents with colonial ambitions', 'Social reformers challenge the rigid class structures of society', 'Spiritualists commune with the dead in dimly lit seances', 'Jack the Ripper stalks the fog-shrouded streets of Whitechapel', 'The industrialists build empires on coal and child labor', 'Suffragettes demand voting rights through peaceful protest', 'Opium dens offer escape from the pressures of imperial duty', 'The great exhibition showcases marvels of modern invention', 'Darwin\'s theories challenge religious doctrines of creation'],
|
|
1051
|
+
ancient_roman: ['Gladiators fight for glory in the Colosseum\'s sandy arena', 'The Roman Senate debates matters of republic and empire', 'Barbarian hordes threaten the borders of civilized lands', 'The aqueducts carry water across vast engineering marvels', 'Political assassinations reshape the corridors of power', 'Slave revolts challenge the foundations of imperial society', 'The gods demand tribute through elaborate public ceremonies', 'Military legions march under eagles forged in sacred fires', 'Patrician families feud over matters of honor and inheritance', 'The imperial palace echoes with plots and betrayals']
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// Seasonal and time-based event templates
|
|
1056
|
+
this.seasonalEvents = {
|
|
1057
|
+
spring: {
|
|
1058
|
+
templates: ['BLOOMING_ROMANCE', 'SPRING_FESTIVAL', 'NEW_BEGINNINGS'],
|
|
1059
|
+
modifiers: {
|
|
1060
|
+
happiness: 1.2,
|
|
1061
|
+
romance_events: 1.5,
|
|
1062
|
+
renewal_events: 2.0
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
summer: {
|
|
1066
|
+
templates: ['SUMMER_TOURNAMENT', 'BEACH_FESTIVAL', 'HARVEST_PREP'],
|
|
1067
|
+
modifiers: {
|
|
1068
|
+
health: 1.1,
|
|
1069
|
+
activity_events: 1.3,
|
|
1070
|
+
celebration_events: 1.8
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
autumn: {
|
|
1074
|
+
templates: ['HARVEST_FESTIVAL', 'AUTUMN_HUNT', 'WINTER_PREP'],
|
|
1075
|
+
modifiers: {
|
|
1076
|
+
wealth: 1.3,
|
|
1077
|
+
preparation_events: 1.6,
|
|
1078
|
+
scarcity_events: 1.2
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
winter: {
|
|
1082
|
+
templates: ['WINTER_SOLSTICE', 'HOLIDAY_CELEBRATION', 'SURVIVAL_CHALLENGE'],
|
|
1083
|
+
modifiers: {
|
|
1084
|
+
stress: 1.4,
|
|
1085
|
+
survival_events: 1.8,
|
|
1086
|
+
warmth_events: 2.0,
|
|
1087
|
+
scarcity_events: 1.5
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// Time-based event chains (events that evolve over time)
|
|
1093
|
+
this.timeBasedEventChains = {
|
|
1094
|
+
POLITICAL_UPRISING: {
|
|
1095
|
+
name: 'Political Uprising',
|
|
1096
|
+
description: 'A rebellion builds against oppressive rule',
|
|
1097
|
+
stages: [{
|
|
1098
|
+
day: 1,
|
|
1099
|
+
template: 'RUMORS_OF_DISSENT'
|
|
1100
|
+
}, {
|
|
1101
|
+
day: 7,
|
|
1102
|
+
template: 'PUBLIC_PROTESTS'
|
|
1103
|
+
}, {
|
|
1104
|
+
day: 14,
|
|
1105
|
+
template: 'OPEN_REBELLION'
|
|
1106
|
+
}, {
|
|
1107
|
+
day: 21,
|
|
1108
|
+
template: 'REVOLUTIONARY_CLIMAX'
|
|
1109
|
+
}]
|
|
1110
|
+
},
|
|
1111
|
+
ECONOMIC_COLLAPSE: {
|
|
1112
|
+
name: 'Economic Collapse',
|
|
1113
|
+
description: 'A financial crisis spirals out of control',
|
|
1114
|
+
stages: [{
|
|
1115
|
+
day: 1,
|
|
1116
|
+
template: 'MARKET_UNREST'
|
|
1117
|
+
}, {
|
|
1118
|
+
day: 5,
|
|
1119
|
+
template: 'BANK_RUN'
|
|
1120
|
+
}, {
|
|
1121
|
+
day: 10,
|
|
1122
|
+
template: 'ECONOMIC_CRISIS'
|
|
1123
|
+
}, {
|
|
1124
|
+
day: 15,
|
|
1125
|
+
template: 'SOCIAL_UNREST'
|
|
1126
|
+
}]
|
|
1127
|
+
},
|
|
1128
|
+
MYSTICAL_AWAKENING: {
|
|
1129
|
+
name: 'Mystical Awakening',
|
|
1130
|
+
description: 'Ancient powers stir and manifest',
|
|
1131
|
+
stages: [{
|
|
1132
|
+
day: 1,
|
|
1133
|
+
template: 'STRANGE_OCCURRENCES'
|
|
1134
|
+
}, {
|
|
1135
|
+
day: 3,
|
|
1136
|
+
template: 'MAGICAL_SURGES'
|
|
1137
|
+
}, {
|
|
1138
|
+
day: 7,
|
|
1139
|
+
template: 'ARCANE_MANIFESTATION'
|
|
1140
|
+
}, {
|
|
1141
|
+
day: 10,
|
|
1142
|
+
template: 'MYSTICAL_CLIMAX'
|
|
1143
|
+
}]
|
|
1144
|
+
}
|
|
600
1145
|
};
|
|
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
1146
|
}
|
|
606
1147
|
|
|
607
1148
|
/**
|
|
608
|
-
*
|
|
1149
|
+
* Get training data for a specific theme and culture
|
|
1150
|
+
* @param {string} theme - The theme to get training data for
|
|
1151
|
+
* @param {string} culture - The cultural variant (optional)
|
|
1152
|
+
* @returns {Array} Training data for the specified theme/culture
|
|
1153
|
+
* @private
|
|
1154
|
+
*/
|
|
1155
|
+
getThemeTrainingData(theme, culture) {
|
|
1156
|
+
const baseThemeData = this.themes[theme] || this.themes.fantasy;
|
|
1157
|
+
if (!culture || !this.cultures[theme] || !this.cultures[theme][culture]) {
|
|
1158
|
+
return baseThemeData;
|
|
1159
|
+
}
|
|
1160
|
+
const culturalData = this.cultures[theme][culture];
|
|
1161
|
+
const blendRatio = 0.6;
|
|
1162
|
+
const baseCount = Math.floor(baseThemeData.length * blendRatio);
|
|
1163
|
+
const cultureCount = Math.floor(culturalData.length * (1 - blendRatio));
|
|
1164
|
+
const blendedData = [...baseThemeData.slice(0, baseCount), ...culturalData.slice(0, cultureCount)];
|
|
1165
|
+
return this.chance.shuffle(blendedData);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Initialize event chain definitions
|
|
1170
|
+
* @private
|
|
1171
|
+
*/
|
|
1172
|
+
initializeChainDefinitions() {
|
|
1173
|
+
return {
|
|
1174
|
+
BANDIT_RISING: {
|
|
1175
|
+
name: 'Bandit Rising',
|
|
1176
|
+
description: 'A bandit threat that escalates over time',
|
|
1177
|
+
stages: [{
|
|
1178
|
+
template: 'BANDIT_AMBUSH',
|
|
1179
|
+
delay: 0,
|
|
1180
|
+
triggerNext: {
|
|
1181
|
+
choice: 'bandit',
|
|
1182
|
+
delay: 3
|
|
1183
|
+
}
|
|
1184
|
+
}, {
|
|
1185
|
+
template: 'BANDIT_KING',
|
|
1186
|
+
delay: 3,
|
|
1187
|
+
triggerNext: {
|
|
1188
|
+
choice: 'hero',
|
|
1189
|
+
delay: 5
|
|
1190
|
+
}
|
|
1191
|
+
}, {
|
|
1192
|
+
template: 'BANDIT_ARMY',
|
|
1193
|
+
delay: 5
|
|
1194
|
+
}]
|
|
1195
|
+
},
|
|
1196
|
+
COURT_SCANDAL_CHAIN: {
|
|
1197
|
+
name: 'Royal Scandal',
|
|
1198
|
+
description: 'A court intrigue that unfolds in multiple acts',
|
|
1199
|
+
stages: [{
|
|
1200
|
+
template: 'COURT_SCANDAL',
|
|
1201
|
+
delay: 0,
|
|
1202
|
+
triggerNext: {
|
|
1203
|
+
choice: 'investigator',
|
|
1204
|
+
delay: 2
|
|
1205
|
+
}
|
|
1206
|
+
}, {
|
|
1207
|
+
template: 'ASSASSINATION_PLOT',
|
|
1208
|
+
delay: 2,
|
|
1209
|
+
triggerNext: {
|
|
1210
|
+
choice: 'intervene',
|
|
1211
|
+
delay: 4
|
|
1212
|
+
}
|
|
1213
|
+
}, {
|
|
1214
|
+
template: 'ROYAL_PURGE',
|
|
1215
|
+
delay: 4
|
|
1216
|
+
}]
|
|
1217
|
+
},
|
|
1218
|
+
CURSE_OF_THE_ARTIFACT: {
|
|
1219
|
+
name: 'Cursed Artifact',
|
|
1220
|
+
description: 'An ancient curse that manifests in stages',
|
|
1221
|
+
stages: [{
|
|
1222
|
+
template: 'ANCIENT_CURSE',
|
|
1223
|
+
delay: 0,
|
|
1224
|
+
triggerNext: {
|
|
1225
|
+
automatic: true,
|
|
1226
|
+
delay: 2
|
|
1227
|
+
}
|
|
1228
|
+
}, {
|
|
1229
|
+
template: 'GHOSTLY_VISITATION',
|
|
1230
|
+
delay: 2,
|
|
1231
|
+
triggerNext: {
|
|
1232
|
+
automatic: true,
|
|
1233
|
+
delay: 3
|
|
1234
|
+
}
|
|
1235
|
+
}, {
|
|
1236
|
+
template: 'FINAL_RECKONING',
|
|
1237
|
+
delay: 3
|
|
1238
|
+
}]
|
|
1239
|
+
},
|
|
1240
|
+
MERCHANT_EMPIRE: {
|
|
1241
|
+
name: 'Rising Merchant Empire',
|
|
1242
|
+
description: 'Building a trade empire with escalating opportunities',
|
|
1243
|
+
stages: [{
|
|
1244
|
+
template: 'TRADE_OPPORTUNITY',
|
|
1245
|
+
delay: 0,
|
|
1246
|
+
triggerNext: {
|
|
1247
|
+
choice: 'invest',
|
|
1248
|
+
delay: 4
|
|
1249
|
+
}
|
|
1250
|
+
}, {
|
|
1251
|
+
template: 'MARKET_CRASH',
|
|
1252
|
+
delay: 4,
|
|
1253
|
+
triggerNext: {
|
|
1254
|
+
choice: 'rebuild',
|
|
1255
|
+
delay: 6
|
|
1256
|
+
}
|
|
1257
|
+
}, {
|
|
1258
|
+
template: 'TRADE_WAR',
|
|
1259
|
+
delay: 6
|
|
1260
|
+
}]
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Start an event chain
|
|
1267
|
+
* @param {string} chainId - ID of the chain to start
|
|
1268
|
+
* @param {Object} playerContext - Player context
|
|
1269
|
+
* @returns {Object|null} First event in the chain or null
|
|
1270
|
+
*/
|
|
1271
|
+
startChain(chainId, playerContext = {}) {
|
|
1272
|
+
const chainDef = this.chainDefinitions[chainId];
|
|
1273
|
+
if (!chainDef) return null;
|
|
1274
|
+
const chain = {
|
|
1275
|
+
id: `chain_${Date.now()}_${this.chance.guid().substring(0, 8)}`,
|
|
1276
|
+
definition: chainId,
|
|
1277
|
+
stage: 0,
|
|
1278
|
+
startTime: Date.now(),
|
|
1279
|
+
playerContext: {
|
|
1280
|
+
...playerContext
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
this.activeChains.set(chain.id, chain);
|
|
1284
|
+
|
|
1285
|
+
// Generate first event in chain
|
|
1286
|
+
return this.generateChainEvent(chain);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Generate an event from an active chain
|
|
1291
|
+
* @param {Object} chain - Chain object
|
|
1292
|
+
* @returns {Object|null} Generated event or null
|
|
1293
|
+
*/
|
|
1294
|
+
generateChainEvent(chain) {
|
|
1295
|
+
const chainDef = this.chainDefinitions[chain.definition];
|
|
1296
|
+
if (!chainDef || chain.stage >= chainDef.stages.length) return null;
|
|
1297
|
+
const stage = chainDef.stages[chain.stage];
|
|
1298
|
+
const template = this.templates[stage.template];
|
|
1299
|
+
if (!template) return null;
|
|
1300
|
+
const context = this.analyzeContext(chain.playerContext);
|
|
1301
|
+
return {
|
|
1302
|
+
id: `event_${chain.id}_${chain.stage}`,
|
|
1303
|
+
title: this.generateDynamicTitle(template, context),
|
|
1304
|
+
description: this.generateRichDescription(template, context),
|
|
1305
|
+
narrative: template.narrative,
|
|
1306
|
+
choices: this.generateContextualChoices(template.choices, context),
|
|
1307
|
+
type: stage.template,
|
|
1308
|
+
consequence: null,
|
|
1309
|
+
context: context,
|
|
1310
|
+
urgency: this.calculateUrgency(template, context),
|
|
1311
|
+
theme: this.determineTheme(template, context),
|
|
1312
|
+
chainId: chain.id,
|
|
1313
|
+
chainStage: chain.stage,
|
|
1314
|
+
isChainEvent: true
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Advance an event chain based on player choice
|
|
1320
|
+
* @param {string} chainId - Chain ID
|
|
1321
|
+
* @param {string} choice - Player choice consequence
|
|
1322
|
+
* @returns {Object|null} Next event in chain or null
|
|
1323
|
+
*/
|
|
1324
|
+
advanceChain(chainId, choice) {
|
|
1325
|
+
const chain = this.activeChains.get(chainId);
|
|
1326
|
+
if (!chain) return null;
|
|
1327
|
+
const chainDef = this.chainDefinitions[chain.definition];
|
|
1328
|
+
const currentStage = chainDef.stages[chain.stage];
|
|
1329
|
+
|
|
1330
|
+
// Check if current stage triggers next based on choice
|
|
1331
|
+
if (currentStage.triggerNext && currentStage.triggerNext.choice === choice) {
|
|
1332
|
+
chain.stage++;
|
|
1333
|
+
chain.playerContext = {
|
|
1334
|
+
...chain.playerContext,
|
|
1335
|
+
lastChoice: choice
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// Schedule next event for game integration
|
|
1339
|
+
if (chain.stage < chainDef.stages.length) {
|
|
1340
|
+
chain.nextEventTime = this.timeSystem.currentDay + currentStage.triggerNext.delay;
|
|
1341
|
+
chain.nextEventTemplate = chainDef.stages[chain.stage].template;
|
|
1342
|
+
// In a real game, save this chain state and check for due events on each game day
|
|
1343
|
+
}
|
|
1344
|
+
return this.generateChainEvent(chain);
|
|
1345
|
+
}
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Get all active chains
|
|
1351
|
+
* @returns {Array} Array of active chain objects
|
|
1352
|
+
*/
|
|
1353
|
+
getActiveChains() {
|
|
1354
|
+
return Array.from(this.activeChains.values());
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* End a chain
|
|
1359
|
+
* @param {string} chainId - Chain ID to end
|
|
1360
|
+
*/
|
|
1361
|
+
endChain(chainId) {
|
|
1362
|
+
this.activeChains.delete(chainId);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Advance game time and trigger time-based events
|
|
1367
|
+
* @param {number} days - Number of days to advance (default: 1)
|
|
1368
|
+
* @returns {Array} Array of time-based events triggered
|
|
1369
|
+
*/
|
|
1370
|
+
advanceTime(days = 1) {
|
|
1371
|
+
const triggeredEvents = [];
|
|
1372
|
+
for (let i = 0; i < days; i++) {
|
|
1373
|
+
this.timeSystem.currentDay++;
|
|
1374
|
+
const seasonLength = 90;
|
|
1375
|
+
const seasonIndex = Math.floor((this.timeSystem.currentDay - 1) / seasonLength) % 4;
|
|
1376
|
+
const seasons = ['spring', 'summer', 'autumn', 'winter'];
|
|
1377
|
+
const newSeason = seasons[seasonIndex];
|
|
1378
|
+
if (newSeason !== this.timeSystem.currentSeason) {
|
|
1379
|
+
this.timeSystem.currentSeason = newSeason;
|
|
1380
|
+
triggeredEvents.push({
|
|
1381
|
+
type: 'seasonal_change',
|
|
1382
|
+
season: newSeason,
|
|
1383
|
+
day: this.timeSystem.currentDay
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Check for time-based event triggers
|
|
1388
|
+
const dayEvents = this.checkTimeBasedEventTriggers();
|
|
1389
|
+
triggeredEvents.push(...dayEvents);
|
|
1390
|
+
}
|
|
1391
|
+
return triggeredEvents;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Check for time-based event triggers
|
|
1396
|
+
* @returns {Array} Array of triggered events
|
|
1397
|
+
* @private
|
|
1398
|
+
*/
|
|
1399
|
+
checkTimeBasedEventTriggers() {
|
|
1400
|
+
const triggeredEvents = [];
|
|
1401
|
+
for (const [chainId, chainData] of this.timeSystem.timeBasedEvents.entries()) {
|
|
1402
|
+
const daysSinceStart = this.timeSystem.currentDay - chainData.startDay;
|
|
1403
|
+
for (const stage of chainData.chain.stages) {
|
|
1404
|
+
if (stage.day === daysSinceStart && !chainData.completedStages.includes(stage.day)) {
|
|
1405
|
+
chainData.completedStages.push(stage.day);
|
|
1406
|
+
const event = {
|
|
1407
|
+
type: 'time_based_chain',
|
|
1408
|
+
chainId: chainId,
|
|
1409
|
+
stage: stage,
|
|
1410
|
+
day: this.timeSystem.currentDay,
|
|
1411
|
+
template: this.templates[stage.template]
|
|
1412
|
+
};
|
|
1413
|
+
triggeredEvents.push(event);
|
|
1414
|
+
break; // Only trigger one stage per day
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (this.chance.bool({
|
|
1419
|
+
likelihood: 10
|
|
1420
|
+
})) {
|
|
1421
|
+
const seasonalData = this.seasonalEvents[this.timeSystem.currentSeason];
|
|
1422
|
+
if (seasonalData && seasonalData.templates.length > 0) {
|
|
1423
|
+
const randomTemplate = this.chance.pickone(seasonalData.templates);
|
|
1424
|
+
if (this.templates[randomTemplate]) {
|
|
1425
|
+
triggeredEvents.push({
|
|
1426
|
+
type: 'seasonal_random',
|
|
1427
|
+
season: this.timeSystem.currentSeason,
|
|
1428
|
+
template: this.templates[randomTemplate],
|
|
1429
|
+
day: this.timeSystem.currentDay
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return triggeredEvents;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Start a time-based event chain
|
|
1439
|
+
* @param {string} chainName - Name of the chain to start
|
|
1440
|
+
* @returns {boolean} Success status
|
|
1441
|
+
*/
|
|
1442
|
+
startTimeBasedChain(chainName) {
|
|
1443
|
+
const chain = this.timeBasedEventChains[chainName];
|
|
1444
|
+
if (!chain) return false;
|
|
1445
|
+
const chainId = `time_chain_${Date.now()}_${this.chance.guid().substring(0, 8)}`;
|
|
1446
|
+
this.timeSystem.timeBasedEvents.set(chainId, {
|
|
1447
|
+
definition: chainName,
|
|
1448
|
+
stage: 0,
|
|
1449
|
+
startDay: this.timeSystem.currentDay,
|
|
1450
|
+
completedStages: new Set()
|
|
1451
|
+
});
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Get current game time information
|
|
1457
|
+
* @returns {Object} Time system state
|
|
1458
|
+
*/
|
|
1459
|
+
getCurrentTime() {
|
|
1460
|
+
var _this$seasonalEvents$;
|
|
1461
|
+
return {
|
|
1462
|
+
day: this.timeSystem.currentDay,
|
|
1463
|
+
season: this.timeSystem.currentSeason,
|
|
1464
|
+
year: Math.ceil(this.timeSystem.currentDay / 360),
|
|
1465
|
+
// Roughly 360 days per year
|
|
1466
|
+
seasonalModifiers: ((_this$seasonalEvents$ = this.seasonalEvents[this.timeSystem.currentSeason]) === null || _this$seasonalEvents$ === void 0 ? void 0 : _this$seasonalEvents$.modifiers) || {}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Advance to the next game day and process any due time-based events
|
|
1472
|
+
* @returns {Array} Array of events that are now ready to trigger
|
|
1473
|
+
*/
|
|
1474
|
+
advanceGameDay() {
|
|
1475
|
+
this.timeSystem.currentDay++;
|
|
1476
|
+
const seasonLength = 90;
|
|
1477
|
+
const seasonIndex = Math.floor((this.timeSystem.currentDay - 1) / seasonLength) % 4;
|
|
1478
|
+
const seasons = ['spring', 'summer', 'autumn', 'winter'];
|
|
1479
|
+
const newSeason = seasons[seasonIndex];
|
|
1480
|
+
if (newSeason !== this.timeSystem.currentSeason) {
|
|
1481
|
+
this.timeSystem.currentSeason = newSeason;
|
|
1482
|
+
}
|
|
1483
|
+
const dueEvents = [];
|
|
1484
|
+
for (const [chainId, chainData] of this.timeSystem.timeBasedEvents.entries()) {
|
|
1485
|
+
const chainDef = this.timeBasedEventChains[chainData.definition];
|
|
1486
|
+
if (!chainDef) continue;
|
|
1487
|
+
const daysSinceStart = this.timeSystem.currentDay - chainData.startDay;
|
|
1488
|
+
for (let i = chainData.stage; i < chainDef.stages.length; i++) {
|
|
1489
|
+
const stage = chainDef.stages[i];
|
|
1490
|
+
if (stage.day <= daysSinceStart && !chainData.completedStages.has(i)) {
|
|
1491
|
+
const event = {
|
|
1492
|
+
type: 'time_based_chain',
|
|
1493
|
+
chainId: chainId,
|
|
1494
|
+
template: this.templates[stage.template],
|
|
1495
|
+
day: this.timeSystem.currentDay,
|
|
1496
|
+
chainStage: i,
|
|
1497
|
+
stageDay: stage.day
|
|
1498
|
+
};
|
|
1499
|
+
dueEvents.push(event);
|
|
1500
|
+
chainData.completedStages.add(i);
|
|
1501
|
+
chainData.stage = Math.max(chainData.stage, i + 1);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (this.chance.bool({
|
|
1506
|
+
likelihood: 10
|
|
1507
|
+
})) {
|
|
1508
|
+
const seasonalData = this.seasonalEvents[this.timeSystem.currentSeason];
|
|
1509
|
+
if (seasonalData && seasonalData.templates.length > 0) {
|
|
1510
|
+
const randomTemplate = this.chance.pickone(seasonalData.templates);
|
|
1511
|
+
if (this.templates[randomTemplate]) {
|
|
1512
|
+
dueEvents.push({
|
|
1513
|
+
type: 'seasonal_random',
|
|
1514
|
+
season: this.timeSystem.currentSeason,
|
|
1515
|
+
template: this.templates[randomTemplate],
|
|
1516
|
+
day: this.timeSystem.currentDay
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return dueEvents;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Get all currently active time-based chains with their status
|
|
1526
|
+
* @returns {Array} Array of chain status objects
|
|
1527
|
+
*/
|
|
1528
|
+
getActiveTimeChains() {
|
|
1529
|
+
const activeChains = [];
|
|
1530
|
+
for (const [chainId, chainData] of this.timeSystem.timeBasedEvents.entries()) {
|
|
1531
|
+
const chainDef = this.timeBasedEventChains[chainData.definition];
|
|
1532
|
+
if (chainDef) {
|
|
1533
|
+
activeChains.push({
|
|
1534
|
+
id: chainId,
|
|
1535
|
+
name: chainDef.name,
|
|
1536
|
+
description: chainDef.description,
|
|
1537
|
+
currentStage: chainData.stage,
|
|
1538
|
+
totalStages: chainDef.stages.length,
|
|
1539
|
+
nextEventDay: chainData.nextEventTime || null,
|
|
1540
|
+
nextEventTemplate: chainData.nextEventTemplate || null,
|
|
1541
|
+
startDay: chainData.startDay,
|
|
1542
|
+
daysActive: this.timeSystem.currentDay - chainData.startDay
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return activeChains;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Get game state for saving (includes time system and active chains)
|
|
1551
|
+
* @returns {Object} Serializable game state
|
|
1552
|
+
*/
|
|
1553
|
+
getGameState() {
|
|
1554
|
+
return {
|
|
1555
|
+
timeSystem: {
|
|
1556
|
+
currentDay: this.timeSystem.currentDay,
|
|
1557
|
+
currentSeason: this.timeSystem.currentSeason,
|
|
1558
|
+
gameYear: Math.ceil(this.timeSystem.currentDay / 360)
|
|
1559
|
+
},
|
|
1560
|
+
activeChains: Array.from(this.timeSystem.timeBasedEvents.entries()).map(([id, data]) => ({
|
|
1561
|
+
id,
|
|
1562
|
+
definition: data.definition,
|
|
1563
|
+
stage: data.stage,
|
|
1564
|
+
startDay: data.startDay,
|
|
1565
|
+
nextEventTime: data.nextEventTime || null,
|
|
1566
|
+
nextEventTemplate: data.nextEventTemplate || null,
|
|
1567
|
+
completedStages: [...data.completedStages]
|
|
1568
|
+
})),
|
|
1569
|
+
theme: this.theme,
|
|
1570
|
+
culture: this.culture
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Load game state (restores time system and active chains)
|
|
1576
|
+
* @param {Object} gameState - State object from getGameState()
|
|
1577
|
+
* @returns {boolean} Success status
|
|
1578
|
+
*/
|
|
1579
|
+
loadGameState(gameState) {
|
|
1580
|
+
try {
|
|
1581
|
+
if (gameState.timeSystem) {
|
|
1582
|
+
this.timeSystem.currentDay = gameState.timeSystem.currentDay || 1;
|
|
1583
|
+
this.timeSystem.currentSeason = gameState.timeSystem.currentSeason || 'spring';
|
|
1584
|
+
this.timeSystem.gameYear = gameState.timeSystem.gameYear || 1;
|
|
1585
|
+
}
|
|
1586
|
+
this.timeSystem.timeBasedEvents.clear();
|
|
1587
|
+
if (gameState.activeChains) {
|
|
1588
|
+
gameState.activeChains.forEach(chainData => {
|
|
1589
|
+
this.timeSystem.timeBasedEvents.set(chainData.id, {
|
|
1590
|
+
definition: chainData.definition,
|
|
1591
|
+
stage: chainData.stage,
|
|
1592
|
+
startDay: chainData.startDay,
|
|
1593
|
+
nextEventTime: chainData.nextEventTime,
|
|
1594
|
+
nextEventTemplate: chainData.nextEventTemplate,
|
|
1595
|
+
completedStages: new Set(chainData.completedStages || [])
|
|
1596
|
+
});
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
if (gameState.theme) this.theme = gameState.theme;
|
|
1600
|
+
if (gameState.culture) this.culture = gameState.culture;
|
|
1601
|
+
return true;
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
console.warn('Failed to load game state:', error.message);
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Generate a time-aware event that considers current season and time-based factors
|
|
1610
|
+
* @param {Object} playerContext - Player context
|
|
1611
|
+
* @returns {Object} Generated event with time awareness
|
|
1612
|
+
*/
|
|
1613
|
+
generateTimeAwareEvent(playerContext = {}) {
|
|
1614
|
+
const context = this.analyzeContext(playerContext);
|
|
1615
|
+
const currentTime = this.getCurrentTime();
|
|
1616
|
+
const seasonalMods = currentTime.seasonalModifiers;
|
|
1617
|
+
Object.keys(seasonalMods).forEach(key => {
|
|
1618
|
+
if (context[key] !== undefined) {
|
|
1619
|
+
context[key] *= seasonalMods[key];
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
const template = this.selectTemplate(context);
|
|
1623
|
+
const scaledChoices = this.scaleEffectsForDifficulty(this.generateContextualChoices(template.choices, context), context);
|
|
1624
|
+
return {
|
|
1625
|
+
id: `event_${Date.now()}_${this.chance.guid().substring(0, 8)}`,
|
|
1626
|
+
title: this.generateDynamicTitle(template, context),
|
|
1627
|
+
description: this.generateRichDescription(template, context),
|
|
1628
|
+
narrative: template.narrative,
|
|
1629
|
+
choices: scaledChoices,
|
|
1630
|
+
type: Object.keys(this.templates).find(key => this.templates[key] === template),
|
|
1631
|
+
consequence: null,
|
|
1632
|
+
context: context,
|
|
1633
|
+
urgency: this.calculateUrgency(template, context),
|
|
1634
|
+
theme: this.determineTheme(template, context),
|
|
1635
|
+
difficulty: this.calculateDifficultyTier(context.power_level || 0),
|
|
1636
|
+
timeInfo: currentTime
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Register a custom event template
|
|
1642
|
+
* @param {string} templateId - Unique identifier for the template
|
|
1643
|
+
* @param {Object} template - Event template object
|
|
1644
|
+
* @param {string} template.title - Event title
|
|
1645
|
+
* @param {string} template.narrative - Event description
|
|
1646
|
+
* @param {Array} template.choices - Array of choice objects
|
|
1647
|
+
* @param {boolean} override - Whether to override existing template
|
|
1648
|
+
* @returns {boolean} Success status
|
|
1649
|
+
*/
|
|
1650
|
+
registerEventTemplate(templateId, template, override = false) {
|
|
1651
|
+
if (!templateId || typeof templateId !== 'string') {
|
|
1652
|
+
console.warn('Invalid template ID provided');
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
if (!template || !template.title || !template.narrative || !Array.isArray(template.choices)) {
|
|
1656
|
+
console.warn('Invalid template structure. Required: title, narrative, choices[]');
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
if (this.templates[templateId] && !override) {
|
|
1660
|
+
console.warn(`Template '${templateId}' already exists. Use override=true to replace it.`);
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
for (const choice of template.choices) {
|
|
1664
|
+
if (!choice.text || typeof choice.text !== 'string') {
|
|
1665
|
+
console.warn(`Invalid choice structure in template '${templateId}': missing 'text'`);
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
this.templates[templateId] = {
|
|
1670
|
+
title: template.title,
|
|
1671
|
+
narrative: template.narrative,
|
|
1672
|
+
choices: template.choices
|
|
1673
|
+
};
|
|
1674
|
+
if (!this.customTemplates) {
|
|
1675
|
+
this.customTemplates = new Set();
|
|
1676
|
+
}
|
|
1677
|
+
this.customTemplates.add(templateId);
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Unregister a custom event template
|
|
1683
|
+
* @param {string} templateId - Template ID to remove
|
|
1684
|
+
* @returns {boolean} Success status
|
|
1685
|
+
*/
|
|
1686
|
+
unregisterEventTemplate(templateId) {
|
|
1687
|
+
if (!this.customTemplates || !this.customTemplates.has(templateId)) {
|
|
1688
|
+
console.warn(`Template '${templateId}' is not a registered custom template`);
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1691
|
+
delete this.templates[templateId];
|
|
1692
|
+
this.customTemplates.delete(templateId);
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Get all registered custom templates
|
|
1698
|
+
* @returns {Array} Array of custom template IDs
|
|
1699
|
+
*/
|
|
1700
|
+
getCustomTemplates() {
|
|
1701
|
+
return this.customTemplates ? Array.from(this.customTemplates) : [];
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Add custom training data for Markov chain generation
|
|
1706
|
+
* @param {Array|string} data - Training data (array of strings or single string)
|
|
1707
|
+
* @param {string} category - Category name for organization
|
|
1708
|
+
* @returns {boolean} Success status
|
|
1709
|
+
*/
|
|
1710
|
+
addCustomTrainingData(data, category) {
|
|
1711
|
+
if (category === undefined) category = 'custom';
|
|
1712
|
+
if (!data) return false;
|
|
1713
|
+
if (!this.customTrainingData) {
|
|
1714
|
+
this.customTrainingData = {};
|
|
1715
|
+
}
|
|
1716
|
+
if (!this.customTrainingData[category]) {
|
|
1717
|
+
this.customTrainingData[category] = [];
|
|
1718
|
+
}
|
|
1719
|
+
const dataArray = Array.isArray(data) ? data : [data];
|
|
1720
|
+
const validData = dataArray.filter(function (item) {
|
|
1721
|
+
return typeof item === 'string' && item.trim().length > 0;
|
|
1722
|
+
});
|
|
1723
|
+
if (validData.length === 0) return false;
|
|
1724
|
+
this.customTrainingData[category] = this.customTrainingData[category].concat(validData);
|
|
1725
|
+
try {
|
|
1726
|
+
const allData = this.getCombinedTrainingData();
|
|
1727
|
+
this.markovGenerator.addData(allData);
|
|
1728
|
+
return true;
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Remove custom training data
|
|
1736
|
+
* @param {string} category - Category to remove (removes all data in category)
|
|
1737
|
+
* @returns {boolean} Success status
|
|
1738
|
+
*/
|
|
1739
|
+
removeTrainingData(category) {
|
|
1740
|
+
if (!this.customTrainingData || !this.customTrainingData[category]) {
|
|
1741
|
+
console.warn(`Training data category '${category}' not found`);
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
delete this.customTrainingData[category];
|
|
1745
|
+
const allData = this.getCombinedTrainingData();
|
|
1746
|
+
try {
|
|
1747
|
+
this.markovGenerator.addData(allData);
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
console.warn('Failed to retrain Markov generator after removal:', error.message);
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
return true;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Get combined training data (base + custom)
|
|
1757
|
+
* @returns {Array} Combined training data array
|
|
1758
|
+
* @private
|
|
1759
|
+
*/
|
|
1760
|
+
getCombinedTrainingData() {
|
|
1761
|
+
let combinedData = [];
|
|
1762
|
+
const themeData = this.getThemeTrainingData(this.theme || 'fantasy', this.culture);
|
|
1763
|
+
combinedData.push(...themeData);
|
|
1764
|
+
if (this.customTrainingData) {
|
|
1765
|
+
Object.values(this.customTrainingData).forEach(categoryData => {
|
|
1766
|
+
combinedData.push(...categoryData);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
return combinedData;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Register a custom event chain
|
|
1774
|
+
* @param {string} chainId - Unique identifier for the chain
|
|
1775
|
+
* @param {Object} chainConfig - Chain configuration
|
|
1776
|
+
* @param {string} chainConfig.name - Display name for the chain
|
|
1777
|
+
* @param {string} chainConfig.description - Chain description
|
|
1778
|
+
* @param {Array} chainConfig.stages - Array of stage objects
|
|
1779
|
+
* @param {boolean} override - Whether to override existing chain
|
|
1780
|
+
* @returns {boolean} Success status
|
|
1781
|
+
*/
|
|
1782
|
+
registerEventChain(chainId, chainConfig, override = false) {
|
|
1783
|
+
if (!chainId || typeof chainId !== 'string') {
|
|
1784
|
+
console.warn('Invalid chain ID provided');
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
if (!chainConfig || !chainConfig.name || !chainConfig.description || !Array.isArray(chainConfig.stages)) {
|
|
1788
|
+
console.warn('Invalid chain configuration. Required: name, description, stages[]');
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
if (this.timeBasedEventChains[chainId] && !override) {
|
|
1792
|
+
console.warn(`Chain '${chainId}' already exists. Use override=true to replace it.`);
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
for (let i = 0; i < chainConfig.stages.length; i++) {
|
|
1796
|
+
const stage = chainConfig.stages[i];
|
|
1797
|
+
if (!stage.day || !stage.template) {
|
|
1798
|
+
console.warn(`Invalid stage ${i} in chain '${chainId}': missing 'day' or 'template'`);
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
if (!this.templates[stage.template]) {
|
|
1802
|
+
console.warn(`Template '${stage.template}' in chain '${chainId}' does not exist`);
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
this.timeBasedEventChains[chainId] = {
|
|
1807
|
+
name: chainConfig.name,
|
|
1808
|
+
description: chainConfig.description,
|
|
1809
|
+
stages: chainConfig.stages
|
|
1810
|
+
};
|
|
1811
|
+
if (!this.customChains) {
|
|
1812
|
+
this.customChains = new Set();
|
|
1813
|
+
}
|
|
1814
|
+
this.customChains.add(chainId);
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Unregister a custom event chain
|
|
1820
|
+
* @param {string} chainId - Chain ID to remove
|
|
1821
|
+
* @returns {boolean} Success status
|
|
1822
|
+
*/
|
|
1823
|
+
unregisterEventChain(chainId) {
|
|
1824
|
+
if (!this.customChains || !this.customChains.has(chainId)) {
|
|
1825
|
+
console.warn(`Chain '${chainId}' is not a registered custom chain`);
|
|
1826
|
+
return false;
|
|
1827
|
+
}
|
|
1828
|
+
delete this.timeBasedEventChains[chainId];
|
|
1829
|
+
this.customChains.delete(chainId);
|
|
1830
|
+
for (const [instanceId, chainData] of this.timeSystem.timeBasedEvents.entries()) {
|
|
1831
|
+
if (chainData.chain === this.timeBasedEventChains[chainId]) {
|
|
1832
|
+
this.timeSystem.timeBasedEvents.delete(instanceId);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return true;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Get all registered custom chains
|
|
1840
|
+
* @returns {Array} Array of custom chain IDs
|
|
1841
|
+
*/
|
|
1842
|
+
getCustomChains() {
|
|
1843
|
+
return this.customChains ? Array.from(this.customChains) : [];
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Export all custom content for backup/sharing
|
|
1848
|
+
* @returns {Object} Exported custom content
|
|
1849
|
+
*/
|
|
1850
|
+
exportCustomContent() {
|
|
1851
|
+
return {
|
|
1852
|
+
templates: Array.from(this.customTemplates || []).reduce((obj, id) => {
|
|
1853
|
+
obj[id] = this.templates[id];
|
|
1854
|
+
return obj;
|
|
1855
|
+
}, {}),
|
|
1856
|
+
trainingData: this.customTrainingData || {},
|
|
1857
|
+
chains: Array.from(this.customChains || []).reduce((obj, id) => {
|
|
1858
|
+
obj[id] = this.timeBasedEventChains[id];
|
|
1859
|
+
return obj;
|
|
1860
|
+
}, {})
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Import custom content from export
|
|
1866
|
+
* @param {Object} content - Exported custom content
|
|
1867
|
+
* @param {boolean} override - Whether to override existing content
|
|
1868
|
+
* @returns {Object} Import results
|
|
1869
|
+
*/
|
|
1870
|
+
importCustomContent(content, override = false) {
|
|
1871
|
+
const results = {
|
|
1872
|
+
templates: {
|
|
1873
|
+
success: 0,
|
|
1874
|
+
failed: 0
|
|
1875
|
+
},
|
|
1876
|
+
trainingData: {
|
|
1877
|
+
success: 0,
|
|
1878
|
+
failed: 0
|
|
1879
|
+
},
|
|
1880
|
+
chains: {
|
|
1881
|
+
success: 0,
|
|
1882
|
+
failed: 0
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
if (content.templates) {
|
|
1886
|
+
Object.entries(content.templates).forEach(([id, template]) => {
|
|
1887
|
+
if (this.registerEventTemplate(id, template, override)) {
|
|
1888
|
+
results.templates.success++;
|
|
1889
|
+
} else {
|
|
1890
|
+
results.templates.failed++;
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
if (content.trainingData) {
|
|
1895
|
+
Object.entries(content.trainingData).forEach(([category, data]) => {
|
|
1896
|
+
if (this.addCustomTrainingData(data, category)) {
|
|
1897
|
+
results.trainingData.success++;
|
|
1898
|
+
} else {
|
|
1899
|
+
results.trainingData.failed++;
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
if (content.chains) {
|
|
1904
|
+
Object.entries(content.chains).forEach(([id, chain]) => {
|
|
1905
|
+
if (this.registerEventChain(id, chain, override)) {
|
|
1906
|
+
results.chains.success++;
|
|
1907
|
+
} else {
|
|
1908
|
+
results.chains.failed++;
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
return results;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* Analyze and enrich player context for sophisticated event generation
|
|
1917
|
+
* @private
|
|
1918
|
+
*/
|
|
1919
|
+
analyzeContext(playerContext) {
|
|
1920
|
+
const ctx = playerContext || {};
|
|
1921
|
+
const context = {
|
|
1922
|
+
age: ctx.age || 16,
|
|
1923
|
+
wealth: ctx.gold || 0,
|
|
1924
|
+
influence: ctx.influence || 0,
|
|
1925
|
+
reputation: ctx.reputation || 0,
|
|
1926
|
+
career: ctx.career || null,
|
|
1927
|
+
skills: ctx.skills || {},
|
|
1928
|
+
relationships: ctx.relationships || [],
|
|
1929
|
+
location: ctx.location || this.generateLocation(),
|
|
1930
|
+
season: ctx.season || this.timeSystem.currentSeason,
|
|
1931
|
+
health: ctx.health || 100,
|
|
1932
|
+
stress: ctx.stress || 0,
|
|
1933
|
+
happiness: ctx.happiness || 50,
|
|
1934
|
+
karma: ctx.karma || 0,
|
|
1935
|
+
faith: ctx.faith || 0,
|
|
1936
|
+
vices: ctx.vices || [],
|
|
1937
|
+
secrets: ctx.secrets || [],
|
|
1938
|
+
ambitions: ctx.ambitions || []
|
|
1939
|
+
};
|
|
1940
|
+
context.social_standing = this.calculateSocialStanding(context);
|
|
1941
|
+
context.power_level = this.calculatePowerLevel(context);
|
|
1942
|
+
context.life_experience = this.calculateLifeExperience(context);
|
|
1943
|
+
return context;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* Calculate social standing based on various factors
|
|
609
1948
|
* @private
|
|
610
1949
|
*/
|
|
611
1950
|
calculateSocialStanding(context) {
|
|
@@ -633,6 +1972,94 @@ class RPGEventGenerator {
|
|
|
633
1972
|
return Math.max(0, power);
|
|
634
1973
|
}
|
|
635
1974
|
|
|
1975
|
+
/**
|
|
1976
|
+
* Calculate appropriate difficulty tier based on player power level
|
|
1977
|
+
* @param {number} powerLevel - Player's calculated power level
|
|
1978
|
+
* @returns {string} Difficulty tier ('easy', 'normal', 'hard', 'legendary')
|
|
1979
|
+
*/
|
|
1980
|
+
calculateDifficultyTier(powerLevel) {
|
|
1981
|
+
if (powerLevel <= 50) return 'easy';
|
|
1982
|
+
if (powerLevel <= 150) return 'normal';
|
|
1983
|
+
if (powerLevel <= 300) return 'hard';
|
|
1984
|
+
return 'legendary';
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* Scale event effects based on difficulty and player power level
|
|
1989
|
+
* @param {Array} choices - Event choices with effects
|
|
1990
|
+
* @param {Object} context - Player context
|
|
1991
|
+
* @returns {Array} Scaled choices
|
|
1992
|
+
*/
|
|
1993
|
+
scaleEffectsForDifficulty(choices, context) {
|
|
1994
|
+
const powerLevel = context.power_level || 0;
|
|
1995
|
+
const difficultyTier = this.calculateDifficultyTier(powerLevel);
|
|
1996
|
+
const settings = this.difficultySettings[difficultyTier];
|
|
1997
|
+
return choices.map(choice => {
|
|
1998
|
+
const scaledChoice = {
|
|
1999
|
+
...choice
|
|
2000
|
+
};
|
|
2001
|
+
if (choice.effect) {
|
|
2002
|
+
scaledChoice.effect = {
|
|
2003
|
+
...choice.effect
|
|
2004
|
+
};
|
|
2005
|
+
Object.keys(scaledChoice.effect).forEach(key => {
|
|
2006
|
+
const value = scaledChoice.effect[key];
|
|
2007
|
+
if (Array.isArray(value)) {
|
|
2008
|
+
if (key.includes('gold') || key.includes('influence') || key.includes('reputation') || key.includes('knowledge') || key.includes('karma')) {
|
|
2009
|
+
scaledChoice.effect[key] = value.map(v => Math.round(v * settings.rewardMultiplier));
|
|
2010
|
+
} else if (key.includes('health') || key.includes('stress')) {
|
|
2011
|
+
scaledChoice.effect[key] = value.map(v => Math.round(v * settings.penaltyMultiplier));
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
return scaledChoice;
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Adjust template weights based on appropriate challenge level
|
|
2022
|
+
* @param {Object} baseWeights - Base template weights
|
|
2023
|
+
* @param {Object} context - Player context
|
|
2024
|
+
* @returns {Object} Adjusted weights
|
|
2025
|
+
*/
|
|
2026
|
+
adjustWeightsForDifficulty(baseWeights, context) {
|
|
2027
|
+
const powerLevel = context.power_level || 0;
|
|
2028
|
+
const difficultyTier = this.calculateDifficultyTier(powerLevel);
|
|
2029
|
+
const challengeRatings = {
|
|
2030
|
+
COURT_SCANDAL: 3,
|
|
2031
|
+
NOBLE_DUEL: 4,
|
|
2032
|
+
THIEVES_GUILD: 5,
|
|
2033
|
+
BLACKMAIL_OPPORTUNITY: 4,
|
|
2034
|
+
ANCIENT_CURSE: 6,
|
|
2035
|
+
GHOSTLY_VISITATION: 5,
|
|
2036
|
+
FORBIDDEN_LOVE: 3,
|
|
2037
|
+
FAMILY_SECRET: 3,
|
|
2038
|
+
LOST_CIVILIZATION: 7,
|
|
2039
|
+
BANDIT_KING: 6,
|
|
2040
|
+
MARKET_CRASH: 4,
|
|
2041
|
+
TRADE_WAR: 5,
|
|
2042
|
+
DESERTION_TEMPTATION: 2,
|
|
2043
|
+
MERCENARY_CONTRACT: 5
|
|
2044
|
+
};
|
|
2045
|
+
const adjustedWeights = {
|
|
2046
|
+
...baseWeights
|
|
2047
|
+
};
|
|
2048
|
+
Object.keys(adjustedWeights).forEach(templateKey => {
|
|
2049
|
+
const rating = challengeRatings[templateKey] || 5;
|
|
2050
|
+
const powerRatio = powerLevel / 100; // Normalize power level
|
|
2051
|
+
|
|
2052
|
+
if (rating < powerRatio + 2) {
|
|
2053
|
+
adjustedWeights[templateKey] *= 0.5;
|
|
2054
|
+
} else if (rating >= powerRatio + 2 && rating <= powerRatio + 5) {
|
|
2055
|
+
adjustedWeights[templateKey] *= 1.5;
|
|
2056
|
+
} else if (rating > powerRatio + 6) {
|
|
2057
|
+
adjustedWeights[templateKey] *= 0.3;
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
return adjustedWeights;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
636
2063
|
/**
|
|
637
2064
|
* Calculate life experience factor
|
|
638
2065
|
* @private
|
|
@@ -676,8 +2103,9 @@ class RPGEventGenerator {
|
|
|
676
2103
|
baseWeights[key] = this.getBaseWeightForTemplate(key, context);
|
|
677
2104
|
});
|
|
678
2105
|
this.applyContextModifiers(baseWeights, context);
|
|
679
|
-
const
|
|
680
|
-
const
|
|
2106
|
+
const difficultyAdjustedWeights = this.adjustWeightsForDifficulty(baseWeights, context);
|
|
2107
|
+
const totalWeight = Object.values(difficultyAdjustedWeights).reduce((sum, w) => sum + w, 0);
|
|
2108
|
+
const normalizedWeights = Object.values(difficultyAdjustedWeights).map(w => w / totalWeight);
|
|
681
2109
|
return normalizedWeights;
|
|
682
2110
|
}
|
|
683
2111
|
|