rpg-event-generator 4.0.0 → 5.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 (95) hide show
  1. package/README.md +48 -371
  2. package/demo.js +80 -1055
  3. package/dist/RPGEventGenerator.d.ts +25 -3
  4. package/dist/RPGEventGenerator.d.ts.map +1 -1
  5. package/dist/RPGEventGenerator.js +52 -26
  6. package/dist/RPGEventGenerator.js.map +1 -1
  7. package/dist/chains/ChainSystem.d.ts +3 -2
  8. package/dist/chains/ChainSystem.d.ts.map +1 -1
  9. package/dist/chains/ChainSystem.js +5 -5
  10. package/dist/chains/ChainSystem.js.map +1 -1
  11. package/dist/chains/index.js +1 -1
  12. package/dist/core/ContextAnalyzer.js +1 -1
  13. package/dist/core/DescriptionFragmentLibrary.d.ts +3 -2
  14. package/dist/core/DescriptionFragmentLibrary.d.ts.map +1 -1
  15. package/dist/core/DescriptionFragmentLibrary.js +4 -4
  16. package/dist/core/DescriptionFragmentLibrary.js.map +1 -1
  17. package/dist/core/DifficultyScaler.js +1 -1
  18. package/dist/core/GeneratorCore.d.ts +4 -43
  19. package/dist/core/GeneratorCore.d.ts.map +1 -1
  20. package/dist/core/GeneratorCore.js +29 -71
  21. package/dist/core/GeneratorCore.js.map +1 -1
  22. package/dist/core/GrammarRulesEngine.d.ts +3 -2
  23. package/dist/core/GrammarRulesEngine.d.ts.map +1 -1
  24. package/dist/core/GrammarRulesEngine.js +4 -4
  25. package/dist/core/GrammarRulesEngine.js.map +1 -1
  26. package/dist/core/MarkovEngine.js +1 -1
  27. package/dist/core/SentenceBuilder.d.ts +3 -2
  28. package/dist/core/SentenceBuilder.d.ts.map +1 -1
  29. package/dist/core/SentenceBuilder.js +7 -36
  30. package/dist/core/SentenceBuilder.js.map +1 -1
  31. package/dist/core/index.js +1 -1
  32. package/dist/database/MemoryDatabaseAdapter.js +1 -1
  33. package/dist/database/TemplateDatabase.js +1 -1
  34. package/dist/environment/EnvironmentalSystem.d.ts +5 -2
  35. package/dist/environment/EnvironmentalSystem.d.ts.map +1 -1
  36. package/dist/environment/EnvironmentalSystem.js +50 -14
  37. package/dist/environment/EnvironmentalSystem.js.map +1 -1
  38. package/dist/environment/index.js +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/interfaces/systems.d.ts +1 -1
  41. package/dist/interfaces/systems.d.ts.map +1 -1
  42. package/dist/localization/LocalizationSystem.js +1 -1
  43. package/dist/localization/index.js +1 -1
  44. package/dist/relationships/RelationshipSystem.js +1 -1
  45. package/dist/relationships/index.js +1 -1
  46. package/dist/rules/RuleEngine.js +1 -1
  47. package/dist/rules/index.js +1 -1
  48. package/dist/src/types/config.d.ts +3 -1
  49. package/dist/src/types/config.d.ts.map +1 -1
  50. package/dist/src/types/world.d.ts +9 -0
  51. package/dist/src/types/world.d.ts.map +1 -1
  52. package/dist/src/utils/random.d.ts +73 -0
  53. package/dist/src/utils/random.d.ts.map +1 -0
  54. package/dist/src/utils/random.js +182 -0
  55. package/dist/src/utils/random.js.map +1 -0
  56. package/dist/templates/TemplateSystem.js +1 -1
  57. package/dist/templates/index.js +1 -1
  58. package/dist/time/TimeSystem.d.ts +3 -2
  59. package/dist/time/TimeSystem.d.ts.map +1 -1
  60. package/dist/time/TimeSystem.js +7 -7
  61. package/dist/time/TimeSystem.js.map +1 -1
  62. package/dist/time/index.js +1 -1
  63. package/dist/types/config.d.ts +3 -1
  64. package/dist/types/config.d.ts.map +1 -1
  65. package/dist/types/world.d.ts +9 -0
  66. package/dist/types/world.d.ts.map +1 -1
  67. package/dist/utils/array.js +1 -1
  68. package/dist/utils/constants.js +1 -1
  69. package/dist/utils/file.js +1 -1
  70. package/dist/utils/index.d.ts +1 -0
  71. package/dist/utils/index.d.ts.map +1 -1
  72. package/dist/utils/index.js +2 -1
  73. package/dist/utils/index.js.map +1 -1
  74. package/dist/utils/random.d.ts +23 -2
  75. package/dist/utils/random.d.ts.map +1 -1
  76. package/dist/utils/random.js +62 -5
  77. package/dist/utils/random.js.map +1 -1
  78. package/dist/utils/text.js +1 -1
  79. package/dist/utils/validation.js +1 -1
  80. package/dist/utils/version.d.ts +8 -0
  81. package/dist/utils/version.d.ts.map +1 -0
  82. package/dist/utils/version.js +11 -0
  83. package/dist/utils/version.js.map +1 -0
  84. package/dist/world/WorldBuildingSystem.d.ts +16 -2
  85. package/dist/world/WorldBuildingSystem.d.ts.map +1 -1
  86. package/dist/world/WorldBuildingSystem.js +364 -155
  87. package/dist/world/WorldBuildingSystem.js.map +1 -1
  88. package/dist/world/index.d.ts +1 -1
  89. package/dist/world/index.d.ts.map +1 -1
  90. package/dist/world/index.js.map +1 -1
  91. package/dist/world/worldContent.d.ts +13 -0
  92. package/dist/world/worldContent.d.ts.map +1 -0
  93. package/dist/world/worldContent.js +109 -0
  94. package/dist/world/worldContent.js.map +1 -0
  95. package/package.json +1 -5
@@ -1,26 +1,48 @@
1
1
  "use strict";
2
- // RPG Event Generator v3.0.0 - World Building System
3
- // Automated world generation, faction management, and historical event simulation
2
+ // RPG Event Generator v4.0.0 - World Building System
4
3
  Object.defineProperty(exports, "__esModule", { value: true });
5
4
  exports.WorldBuildingSystem = void 0;
5
+ const random_1 = require("../utils/random");
6
+ const worldContent_1 = require("./worldContent");
6
7
  class WorldBuildingSystem {
7
8
  constructor() {
8
9
  this.regions = new Map();
9
10
  this.factions = new Map();
10
11
  this.historicalEvents = [];
11
12
  this.currentYear = 1000;
12
- }
13
- generateWorld(seed) {
14
- const regions = this.generateRegions(seed);
13
+ this.historyStartYear = 800;
14
+ this.rng = new random_1.SeededRandom();
15
+ this.landmarkCounter = 0;
16
+ }
17
+ generateWorld(seedOrOptions) {
18
+ const options = this.normalizeOptions(seedOrOptions);
19
+ this.regions.clear();
20
+ this.factions.clear();
21
+ this.historicalEvents = [];
22
+ this.currentYear = options.currentYear ?? 1000;
23
+ this.historyStartYear = options.historyStartYear ?? 800;
24
+ this.landmarkCounter = 0;
25
+ this.rng = options.seed !== undefined ? new random_1.SeededRandom(options.seed) : new random_1.SeededRandom();
26
+ const regions = this.generateRegions(options.continentCount ?? 5);
15
27
  const factions = this.generateFactions(regions);
28
+ this.initializeRelationships(factions, regions);
16
29
  const events = this.generateInitialHistory(regions, factions);
30
+ this.historicalEvents.push(...events);
17
31
  return { regions, factions, events };
18
32
  }
19
- generateRegions(seed) {
33
+ normalizeOptions(seedOrOptions) {
34
+ if (typeof seedOrOptions === 'number') {
35
+ return { seed: seedOrOptions };
36
+ }
37
+ return seedOrOptions ?? {};
38
+ }
39
+ generateRegions(continentCount) {
20
40
  const regions = [];
21
- const continentNames = ['Eldoria', 'Drakoria', 'Sylvoria', 'Aquilon', 'Ignisia'];
22
- const kingdomNames = ['Northern Realms', 'Southern Kingdoms', 'Eastern Empire', 'Western Isles', 'Central Dominion'];
23
- continentNames.forEach((name, i) => {
41
+ const count = Math.max(2, Math.min(8, continentCount));
42
+ const continents = this.rng.pickset(worldContent_1.CONTINENT_NAMES, count);
43
+ continents.forEach((name, i) => {
44
+ const culture = this.rng.pickone(['Human', 'Elven', 'Dwarven', 'Orcish']);
45
+ const climate = this.rng.pickone(['temperate', 'tropical', 'arctic', 'desert']);
24
46
  const continent = {
25
47
  id: `continent_${i}`,
26
48
  name,
@@ -28,26 +50,35 @@ class WorldBuildingSystem {
28
50
  sub_regions: [],
29
51
  landmarks: [],
30
52
  resources: [],
31
- population: Math.floor(Math.random() * 10000000) + 5000000,
32
- culture: ['Human', 'Elven', 'Dwarven', 'Orcish'][Math.floor(Math.random() * 4)],
33
- climate: ['temperate', 'tropical', 'arctic', 'desert'][Math.floor(Math.random() * 4)],
34
- political_stability: Math.random() * 0.8 + 0.2,
35
- economic_prosperity: Math.random() * 0.8 + 0.2
53
+ population: this.rng.integer({ min: 5000000, max: 14999999 }),
54
+ culture,
55
+ climate,
56
+ political_stability: this.rng.floating({ min: 0.2, max: 1.0 }),
57
+ economic_prosperity: this.rng.floating({ min: 0.2, max: 1.0 })
36
58
  };
37
- for (let j = 0; j < 2 + Math.floor(Math.random() * 3); j++) {
59
+ const kingdomCount = this.rng.integer({ min: 2, max: 4 });
60
+ const usedPrefixes = new Set();
61
+ for (let j = 0; j < kingdomCount; j++) {
62
+ let prefix = this.rng.pickone(worldContent_1.KINGDOM_PREFIXES);
63
+ while (usedPrefixes.has(prefix) && usedPrefixes.size < worldContent_1.KINGDOM_PREFIXES.length) {
64
+ prefix = this.rng.pickone(worldContent_1.KINGDOM_PREFIXES);
65
+ }
66
+ usedPrefixes.add(prefix);
67
+ const suffix = this.rng.pickone(worldContent_1.KINGDOM_SUFFIXES);
68
+ const kingdomId = `kingdom_${i}_${j}`;
38
69
  const kingdom = {
39
- id: `kingdom_${i}_${j}`,
40
- name: `${kingdomNames[j % kingdomNames.length]} of ${name}`,
70
+ id: kingdomId,
71
+ name: `${prefix} ${suffix} of ${name}`,
41
72
  type: 'kingdom',
42
73
  parent_region: continent.id,
43
74
  sub_regions: [],
44
- landmarks: this.generateLandmarks(2 + Math.floor(Math.random() * 3)),
45
- resources: this.generateResources(3 + Math.floor(Math.random() * 4)),
46
- population: Math.floor(Math.random() * 2000000) + 500000,
47
- culture: continent.culture,
48
- climate: continent.climate,
49
- political_stability: Math.max(0.1, continent.political_stability + (Math.random() - 0.5) * 0.4),
50
- economic_prosperity: Math.max(0.1, continent.economic_prosperity + (Math.random() - 0.5) * 0.4)
75
+ landmarks: this.generateLandmarks(this.rng.integer({ min: 2, max: 4 }), kingdomId, climate, culture),
76
+ resources: this.generateResources(this.rng.integer({ min: 3, max: 6 }), climate),
77
+ population: this.rng.integer({ min: 500000, max: 2499999 }),
78
+ culture,
79
+ climate,
80
+ political_stability: Math.max(0.1, continent.political_stability + this.rng.floating({ min: -0.2, max: 0.2 })),
81
+ economic_prosperity: Math.max(0.1, continent.economic_prosperity + this.rng.floating({ min: -0.2, max: 0.2 }))
51
82
  };
52
83
  continent.sub_regions.push(kingdom.id);
53
84
  regions.push(kingdom);
@@ -57,148 +88,263 @@ class WorldBuildingSystem {
57
88
  regions.forEach(region => this.regions.set(region.id, region));
58
89
  return Array.from(this.regions.values());
59
90
  }
60
- generateLandmarks(count) {
61
- const landmarkTypes = ['castle', 'temple', 'ruins', 'mountain', 'forest', 'lake', 'monument'];
91
+ generateLandmarks(count, regionId, climate, culture) {
92
+ const landmarkTypes = [
93
+ 'castle', 'temple', 'ruins', 'mountain', 'forest', 'lake', 'monument'
94
+ ];
62
95
  const landmarks = [];
63
96
  for (let i = 0; i < count; i++) {
97
+ const type = this.rng.pickone(landmarkTypes);
98
+ const adjective = this.rng.pickone(worldContent_1.LANDMARK_ADJECTIVES);
99
+ const noun = this.rng.pickone(worldContent_1.LANDMARK_NOUNS[type]);
100
+ const id = `landmark_${regionId}_${this.landmarkCounter++}`;
64
101
  landmarks.push({
65
- id: `landmark_${i}`,
66
- name: `Ancient ${landmarkTypes[i % landmarkTypes.length].charAt(0).toUpperCase() + landmarkTypes[i % landmarkTypes.length].slice(1)}`,
67
- type: landmarkTypes[i % landmarkTypes.length],
68
- significance: Math.random() * 10,
69
- description: `A significant ${landmarkTypes[i % landmarkTypes.length]} with historical importance.`,
70
- discovered: Math.random() > 0.3
102
+ id,
103
+ name: `${adjective} ${noun}`,
104
+ type,
105
+ significance: this.rng.floating({ min: 0, max: 10 }),
106
+ description: `A ${adjective.toLowerCase()} ${type} sacred to ${culture} folk in the ${climate} lands.`,
107
+ discovered: this.rng.bool({ likelihood: 70 })
71
108
  });
72
109
  }
73
110
  return landmarks;
74
111
  }
75
- generateResources(count) {
76
- const resourceTypes = ['gold', 'iron', 'wood', 'grain', 'magic_crystals', 'herbs'];
112
+ generateResources(count, climate) {
113
+ const pool = worldContent_1.CLIMATE_RESOURCES[climate] ?? worldContent_1.CLIMATE_RESOURCES.temperate;
114
+ const resourceTypes = pool;
77
115
  const resources = [];
116
+ const used = new Set();
78
117
  for (let i = 0; i < count; i++) {
118
+ let type = this.rng.pickone(resourceTypes);
119
+ let attempts = 0;
120
+ while (used.has(type) && attempts < 10) {
121
+ type = this.rng.pickone(resourceTypes);
122
+ attempts++;
123
+ }
124
+ used.add(type);
79
125
  resources.push({
80
- type: resourceTypes[i % resourceTypes.length],
81
- abundance: Math.random() * 10,
82
- quality: Math.random() * 10
126
+ type,
127
+ abundance: this.rng.floating({ min: 0, max: 10 }),
128
+ quality: this.rng.floating({ min: 0, max: 10 })
83
129
  });
84
130
  }
85
131
  return resources;
86
132
  }
87
133
  generateFactions(regions) {
88
134
  const factions = [];
89
- const factionTypes = ['kingdom', 'guild', 'cult', 'tribe', 'merchants', 'nobles'];
90
- regions.filter(r => r.type === 'kingdom').forEach((kingdom, i) => {
135
+ const factionTypes = [
136
+ 'kingdom', 'guild', 'cult', 'tribe', 'merchants', 'nobles'
137
+ ];
138
+ regions
139
+ .filter(r => r.type === 'kingdom')
140
+ .forEach((kingdom, i) => {
141
+ const type = factionTypes[i % factionTypes.length];
91
142
  const faction = {
92
143
  id: `faction_${i}`,
93
- name: `${kingdom.name} ${factionTypes[i % factionTypes.length].charAt(0).toUpperCase() + factionTypes[i % factionTypes.length].slice(1)}`,
94
- type: factionTypes[i % factionTypes.length],
95
- leader: `Leader of ${kingdom.name}`,
144
+ name: this.generateFactionName(type, kingdom),
145
+ type,
146
+ leader: this.generateLeaderName(kingdom.culture, kingdom.name),
96
147
  home_region: kingdom.id,
97
- influence: Math.random() * 10,
98
- reputation: Math.random() * 10 - 5,
148
+ influence: this.rng.floating({ min: 0, max: 10 }),
149
+ reputation: this.rng.floating({ min: -5, max: 5 }),
99
150
  resources: {
100
- gold: Math.floor(Math.random() * 10000),
101
- influence: Math.floor(Math.random() * 100)
151
+ gold: this.rng.integer({ min: 0, max: 9999 }),
152
+ influence: this.rng.integer({ min: 0, max: 99 })
102
153
  },
103
154
  relationships: {},
104
- goals: ['Expand territory', 'Increase wealth', 'Gain political power']
155
+ goals: this.rng.pickset(worldContent_1.FACTION_GOALS[type] ?? worldContent_1.FACTION_GOALS.kingdom, 2)
105
156
  };
106
157
  factions.push(faction);
107
158
  });
159
+ return factions;
160
+ }
161
+ generateFactionName(type, kingdom) {
162
+ const shortName = kingdom.name.split(' of ')[0];
163
+ switch (type) {
164
+ case 'kingdom':
165
+ return kingdom.name;
166
+ case 'guild':
167
+ return `The ${this.rng.pickone(worldContent_1.LANDMARK_ADJECTIVES)} ${this.rng.pickone(['Smiths', 'Arcane', 'Shipwrights', 'Healers'])} Guild`;
168
+ case 'cult':
169
+ return `Cult of ${this.rng.pickone(worldContent_1.CULT_DEITIES)}`;
170
+ case 'tribe':
171
+ return `${shortName} ${this.rng.pickone(['Clans', 'Horde', 'Kin', 'People'])}`;
172
+ case 'merchants':
173
+ return `${shortName} Merchant League`;
174
+ case 'nobles':
175
+ return `House ${this.rng.pickone(worldContent_1.LEADER_NAMES)} of ${kingdom.name.split(' of ')[1] ?? kingdom.name}`;
176
+ default:
177
+ return kingdom.name;
178
+ }
179
+ }
180
+ generateLeaderName(culture, realm) {
181
+ const titles = worldContent_1.LEADER_TITLES[culture] ?? worldContent_1.LEADER_TITLES.Human;
182
+ const title = this.rng.pickone(titles);
183
+ const name = this.rng.pickone(worldContent_1.LEADER_NAMES);
184
+ return `${title} ${name} of ${realm.split(' of ')[0]}`;
185
+ }
186
+ initializeRelationships(factions, regions) {
108
187
  factions.forEach(faction => {
109
- factions.forEach(otherFaction => {
110
- if (faction.id !== otherFaction.id) {
111
- faction.relationships[otherFaction.id] = Math.random() * 10 - 5;
188
+ factions.forEach(other => {
189
+ if (faction.id === other.id)
190
+ return;
191
+ let relationship = this.rng.floating({ min: -5, max: 5 });
192
+ if (this.areNeighborFactions(faction, other, regions)) {
193
+ relationship = this.rng.bool({ likelihood: 45 })
194
+ ? this.rng.floating({ min: -5, max: -0.5 })
195
+ : this.rng.floating({ min: 0.5, max: 5 });
112
196
  }
197
+ faction.relationships[other.id] = relationship;
113
198
  });
114
199
  this.factions.set(faction.id, faction);
115
200
  });
116
- return Array.from(this.factions.values());
201
+ }
202
+ areNeighborFactions(a, b, regions) {
203
+ const regionA = regions.find(r => r.id === a.home_region);
204
+ const regionB = regions.find(r => r.id === b.home_region);
205
+ if (!regionA || !regionB)
206
+ return false;
207
+ return regionA.parent_region === regionB.parent_region && regionA.id !== regionB.id;
117
208
  }
118
209
  generateInitialHistory(regions, factions) {
119
210
  const events = [];
120
- for (let year = 800; year <= this.currentYear; year += 50 + Math.floor(Math.random() * 100)) {
121
- if (Math.random() > 0.7) {
211
+ for (let year = this.historyStartYear; year <= this.currentYear; year += 50 + this.rng.integer({ min: 0, max: 99 })) {
212
+ if (this.rng.bool({ likelihood: 30 })) {
122
213
  const event = this.generateHistoricalEvent(year, regions, factions);
123
214
  events.push(event);
124
215
  this.applyHistoricalConsequences(event);
125
216
  }
126
217
  }
218
+ if (events.length === 0) {
219
+ const span = Math.max(1, this.currentYear - this.historyStartYear);
220
+ const year = this.historyStartYear + this.rng.integer({ min: 0, max: span });
221
+ const event = this.generateHistoricalEvent(year, regions, factions);
222
+ events.push(event);
223
+ this.applyHistoricalConsequences(event);
224
+ }
127
225
  return events;
128
226
  }
129
227
  generateHistoricalEvent(year, regions, factions) {
130
- const eventTypes = ['war', 'alliance', 'discovery', 'disaster', 'ascension', 'fall', 'plague', 'famine', 'revolution', 'invasion', 'treaty', 'betrayal'];
131
- const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
132
- const numFactions = Math.max(1, Math.min(4, Math.floor(Math.random() * 3) + 1));
133
- const involvedFactions = factions
134
- .sort(() => Math.random() - 0.5)
135
- .slice(0, numFactions);
228
+ const eventTypes = [
229
+ 'war', 'alliance', 'discovery', 'disaster', 'ascension', 'fall',
230
+ 'plague', 'famine', 'revolution', 'invasion', 'treaty', 'betrayal'
231
+ ];
232
+ const eventType = this.rng.pickone(eventTypes);
233
+ const involvedFactions = this.pickFactionsForEvent(eventType, factions);
136
234
  const affectedRegions = regions
137
- .filter(r => involvedFactions.some(f => f.home_region === r.id || r.sub_regions.includes(f.home_region)))
235
+ .filter(r => r.type === 'kingdom' &&
236
+ involvedFactions.some(f => f.home_region === r.id))
138
237
  .map(r => r.id);
238
+ const primaryRegion = regions.find(r => r.id === involvedFactions[0]?.home_region);
139
239
  const event = {
140
- id: `event_${year}_${Math.random().toString(36).substr(2, 9)}`,
240
+ id: `event_${year}_${this.rng.string({ length: 9, pool: 'abcdefghijklmnopqrstuvwxyz0123456789' })}`,
141
241
  year,
142
- title: this.generateEventTitle(eventType, involvedFactions),
143
- description: this.generateEventDescription(eventType, involvedFactions),
242
+ title: this.generateEventTitle(eventType, involvedFactions, primaryRegion),
243
+ description: this.generateEventDescription(eventType, involvedFactions, primaryRegion),
144
244
  type: eventType,
145
- regions_affected: affectedRegions,
245
+ regions_affected: affectedRegions.length > 0 ? affectedRegions : involvedFactions.map(f => f.home_region),
146
246
  factions_involved: involvedFactions.map(f => f.id),
147
- consequences: this.generateAdvancedConsequences(eventType, involvedFactions, affectedRegions, regions),
148
- significance: this.calculateEventSignificance(eventType, involvedFactions.length, affectedRegions.length)
247
+ consequences: this.generateAdvancedConsequences(eventType, involvedFactions, affectedRegions.length > 0 ? affectedRegions : involvedFactions.map(f => f.home_region), regions),
248
+ significance: this.calculateEventSignificance(eventType, involvedFactions.length, Math.max(1, affectedRegions.length))
149
249
  };
150
250
  return event;
151
251
  }
152
- generateEventTitle(type, factions) {
153
- const factionName = factions[0]?.name || 'Unknown';
154
- const secondaryFaction = factions[1]?.name || 'Unknown';
155
- switch (type) {
156
- case 'war': return factions.length > 1 ? `${factionName} vs ${secondaryFaction} War` : `${factionName} Conquest`;
157
- case 'alliance': return `${factionName} Alliance`;
158
- case 'discovery': return `${factionName} Discovery`;
159
- case 'disaster': return `${factionName} Disaster`;
160
- case 'ascension': return `${factionName} Ascension`;
161
- case 'fall': return `${factionName} Fall`;
162
- case 'plague': return `The ${factionName} Plague`;
163
- case 'famine': return `${factionName} Famine`;
164
- case 'revolution': return `${factionName} Revolution`;
165
- case 'invasion': return `${factionName} Invasion`;
166
- case 'treaty': return `${factionName} Treaty`;
167
- case 'betrayal': return factions.length > 1 ? `${factionName} Betrays ${secondaryFaction}` : `${factionName} Betrayal`;
168
- default: return `${factionName} Event`;
252
+ pickFactionsForEvent(eventType, factions) {
253
+ if (factions.length === 0)
254
+ return [];
255
+ if (factions.length === 1)
256
+ return [factions[0]];
257
+ const shuffled = this.rng.shuffle([...factions]);
258
+ const pairTypes = [
259
+ 'war', 'alliance', 'betrayal', 'treaty', 'invasion'
260
+ ];
261
+ if (!pairTypes.includes(eventType)) {
262
+ return [this.rng.pickone(shuffled)];
263
+ }
264
+ if (['war', 'invasion', 'betrayal'].includes(eventType)) {
265
+ for (const faction of shuffled) {
266
+ const enemies = Object.entries(faction.relationships)
267
+ .filter(([_, value]) => value < -1)
268
+ .map(([id]) => this.factions.get(id))
269
+ .filter((f) => !!f);
270
+ if (enemies.length > 0) {
271
+ return [faction, this.rng.pickone(enemies)];
272
+ }
273
+ }
274
+ }
275
+ if (['alliance', 'treaty'].includes(eventType)) {
276
+ for (const faction of shuffled) {
277
+ const allies = Object.entries(faction.relationships)
278
+ .filter(([_, value]) => value > 1)
279
+ .map(([id]) => this.factions.get(id))
280
+ .filter((f) => !!f);
281
+ if (allies.length > 0) {
282
+ return [faction, this.rng.pickone(allies)];
283
+ }
284
+ }
169
285
  }
286
+ return shuffled.slice(0, 2);
170
287
  }
171
- generateEventDescription(type, factions) {
172
- const factionName = factions[0]?.name || 'Unknown';
173
- const secondaryFaction = factions[1]?.name || 'Unknown';
288
+ generateEventTitle(type, factions, region) {
289
+ const place = region?.name ?? factions[0]?.name ?? 'Unknown';
290
+ const factionA = factions[0]?.name ?? 'Unknown';
291
+ const factionB = factions[1]?.name ?? 'Unknown';
174
292
  switch (type) {
175
- case 'war': return factions.length > 1 ?
176
- `${factionName} and ${secondaryFaction} clashed in a brutal conflict that lasted for years, with neither side gaining a decisive victory.` :
177
- `${factionName} waged war against neighboring territories, conquering new lands and expanding their influence.`;
178
- case 'alliance': return `${factionName} formed a powerful alliance with neighboring factions, creating a bloc that dominated regional politics.`;
179
- case 'discovery': return `${factionName} made groundbreaking discoveries in ancient ruins, unlocking powerful artifacts and knowledge.`;
180
- case 'disaster': return `A catastrophic disaster struck ${factionName}, destroying infrastructure and claiming countless lives.`;
181
- case 'ascension': return `Through cunning diplomacy and military prowess, ${factionName} rose from obscurity to become a major power.`;
182
- case 'fall': return `${factionName} suffered a dramatic decline due to corruption, military defeats, and economic collapse.`;
183
- case 'plague': return `A devastating plague swept through ${factionName}'s territories, decimating the population and weakening their society.`;
184
- case 'famine': return `Severe famine gripped ${factionName}, causing widespread starvation and social unrest.`;
185
- case 'revolution': return `The people of ${factionName} rose up in revolution, overthrowing the ruling class and establishing a new order.`;
186
- case 'invasion': return `${factionName} suffered a devastating invasion that destroyed their armies and occupied their lands.`;
187
- case 'treaty': return `${factionName} negotiated a historic treaty that brought peace and prosperity to the region.`;
188
- case 'betrayal': return factions.length > 1 ?
189
- `${factionName} betrayed their longtime ally ${secondaryFaction}, shattering trust and igniting new conflicts.` :
190
- `${factionName} suffered a catastrophic betrayal from within their own ranks.`;
191
- default: return `${factionName} experienced a significant historical event that shaped their destiny.`;
293
+ case 'war':
294
+ return factions.length > 1 ? `War of ${place}` : `${factionA} Conquest`;
295
+ case 'alliance':
296
+ return `Pact of ${place}`;
297
+ case 'discovery':
298
+ return `Discovery at ${place}`;
299
+ case 'disaster':
300
+ return `Calamity in ${place}`;
301
+ case 'ascension':
302
+ return `Rise of ${factionA}`;
303
+ case 'fall':
304
+ return `Fall of ${factionA}`;
305
+ case 'plague':
306
+ return `Plague of ${place}`;
307
+ case 'famine':
308
+ return `Famine in ${place}`;
309
+ case 'revolution':
310
+ return `Revolt in ${place}`;
311
+ case 'invasion':
312
+ return `Invasion of ${place}`;
313
+ case 'treaty':
314
+ return `Treaty of ${place}`;
315
+ case 'betrayal':
316
+ return factions.length > 1 ? `${factionA} Betrays ${factionB}` : `Betrayal at ${place}`;
317
+ default:
318
+ return `${place} Turning Point`;
192
319
  }
193
320
  }
321
+ generateEventDescription(type, factions, region) {
322
+ const templates = worldContent_1.HISTORICAL_TEMPLATES[type];
323
+ const template = templates ? this.rng.pickone(templates) : null;
324
+ const landmark = region?.landmarks?.length
325
+ ? this.rng.pickone(region.landmarks).name
326
+ : 'a border fort';
327
+ const vars = {
328
+ factionA: factions[0]?.name ?? 'Unknown',
329
+ factionB: factions[1]?.name ?? 'a rival power',
330
+ region: region?.name ?? 'the borderlands',
331
+ landmark,
332
+ climate: region?.climate ?? 'temperate',
333
+ culture: region?.culture ?? 'Human'
334
+ };
335
+ if (template) {
336
+ return (0, worldContent_1.formatTemplate)(template, vars);
337
+ }
338
+ return `${vars.factionA} shaped the fate of ${vars.region} near ${vars.landmark}.`;
339
+ }
194
340
  generateConsequences(type, factions, regions) {
195
341
  const allRegions = Array.from(this.regions.values());
196
342
  return this.generateAdvancedConsequences(type, factions, regions, allRegions);
197
343
  }
198
- generateAdvancedConsequences(eventType, factions, regions, allRegions) {
344
+ generateAdvancedConsequences(eventType, factions, regionIds, allRegions) {
199
345
  const consequences = [];
200
346
  factions.forEach(faction => {
201
- const baseChange = Math.random() - 0.5;
347
+ const baseChange = this.rng.floating({ min: -0.5, max: 0.5 });
202
348
  switch (eventType) {
203
349
  case 'war':
204
350
  consequences.push({
@@ -238,7 +384,7 @@ class WorldBuildingSystem {
238
384
  target_id: faction.id,
239
385
  property: 'resources.gold',
240
386
  old_value: faction.resources.gold,
241
- new_value: faction.resources.gold + Math.floor(Math.random() * 10000)
387
+ new_value: faction.resources.gold + this.rng.integer({ min: 0, max: 9999 })
242
388
  });
243
389
  consequences.push({
244
390
  type: 'faction_change',
@@ -250,10 +396,10 @@ class WorldBuildingSystem {
250
396
  break;
251
397
  case 'disaster':
252
398
  case 'plague':
253
- case 'famine':
399
+ case 'famine': {
254
400
  const populationRegion = allRegions.find(r => r.id === faction.home_region);
255
401
  if (populationRegion) {
256
- const populationLoss = Math.floor(populationRegion.population * (0.05 + Math.random() * 0.15));
402
+ const populationLoss = Math.floor(populationRegion.population * (0.05 + this.rng.floating({ min: 0, max: 0.15 })));
257
403
  consequences.push({
258
404
  type: 'region_change',
259
405
  target_id: faction.home_region,
@@ -270,6 +416,7 @@ class WorldBuildingSystem {
270
416
  new_value: Math.max(0, faction.influence - Math.abs(baseChange) * 3)
271
417
  });
272
418
  break;
419
+ }
273
420
  case 'revolution':
274
421
  consequences.push({
275
422
  type: 'faction_change',
@@ -290,9 +437,8 @@ class WorldBuildingSystem {
290
437
  break;
291
438
  }
292
439
  });
293
- // Add regional consequences for certain events
294
440
  if (['disaster', 'plague', 'famine', 'invasion'].includes(eventType)) {
295
- regions.forEach(regionId => {
441
+ regionIds.forEach(regionId => {
296
442
  const region = allRegions.find(r => r.id === regionId);
297
443
  if (region) {
298
444
  consequences.push({
@@ -300,17 +446,37 @@ class WorldBuildingSystem {
300
446
  target_id: regionId,
301
447
  property: 'political_stability',
302
448
  old_value: region.political_stability,
303
- new_value: Math.max(0.1, region.political_stability - Math.random() * 0.3)
449
+ new_value: Math.max(0.1, region.political_stability - this.rng.floating({ min: 0, max: 0.3 }))
304
450
  });
305
451
  }
306
452
  });
307
453
  }
454
+ if (['alliance', 'treaty'].includes(eventType) && factions.length > 1) {
455
+ const [a, b] = factions;
456
+ consequences.push({
457
+ type: 'relationship_change',
458
+ target_id: a.id,
459
+ property: b.id,
460
+ old_value: a.relationships[b.id],
461
+ new_value: Math.min(10, (a.relationships[b.id] ?? 0) + 2)
462
+ });
463
+ }
464
+ if (['war', 'betrayal', 'invasion'].includes(eventType) && factions.length > 1) {
465
+ const [a, b] = factions;
466
+ consequences.push({
467
+ type: 'relationship_change',
468
+ target_id: a.id,
469
+ property: b.id,
470
+ old_value: a.relationships[b.id],
471
+ new_value: Math.max(-10, (a.relationships[b.id] ?? 0) - 2)
472
+ });
473
+ }
308
474
  return consequences;
309
475
  }
310
476
  applyHistoricalConsequences(event) {
311
477
  event.consequences.forEach(consequence => {
312
478
  switch (consequence.type) {
313
- case 'faction_change':
479
+ case 'faction_change': {
314
480
  const faction = this.factions.get(consequence.target_id);
315
481
  if (faction) {
316
482
  if (consequence.property.includes('.')) {
@@ -324,15 +490,54 @@ class WorldBuildingSystem {
324
490
  }
325
491
  }
326
492
  break;
327
- case 'region_change':
493
+ }
494
+ case 'region_change': {
328
495
  const region = this.regions.get(consequence.target_id);
329
496
  if (region) {
330
497
  region[consequence.property] = consequence.new_value;
331
498
  }
332
499
  break;
500
+ }
501
+ case 'relationship_change': {
502
+ const faction = this.factions.get(consequence.target_id);
503
+ const partner = this.factions.get(consequence.property);
504
+ if (faction && partner) {
505
+ faction.relationships[partner.id] = consequence.new_value;
506
+ partner.relationships[faction.id] = consequence.new_value;
507
+ }
508
+ break;
509
+ }
333
510
  }
334
511
  });
335
512
  }
513
+ /** Find a kingdom or region whose name matches the given location string. */
514
+ findRegionByLocation(location) {
515
+ if (!location)
516
+ return undefined;
517
+ const query = location.toLowerCase();
518
+ return Array.from(this.regions.values()).find(r => r.name.toLowerCase().includes(query) || r.id.toLowerCase() === query);
519
+ }
520
+ /** One-sentence lore hook for a player location — useful when layering world context onto events. */
521
+ getLoreSnippet(location) {
522
+ const region = this.findRegionByLocation(location);
523
+ if (!region)
524
+ return null;
525
+ const regionalEvents = this.historicalEvents
526
+ .filter(e => e.regions_affected.includes(region.id))
527
+ .sort((a, b) => b.year - a.year);
528
+ if (regionalEvents.length === 0) {
529
+ const faction = Array.from(this.factions.values()).find(f => f.home_region === region.id);
530
+ if (faction) {
531
+ if (faction.name === region.name) {
532
+ return `${region.name} is ruled by ${faction.leader}.`;
533
+ }
534
+ return `${region.name} is held by ${faction.name}, led by ${faction.leader}.`;
535
+ }
536
+ return `${region.name} is a ${region.climate} ${region.type} of the ${region.culture} peoples.`;
537
+ }
538
+ const latest = regionalEvents[0];
539
+ return `In ${latest.year}, ${latest.title.toLowerCase()} — ${latest.description}`;
540
+ }
336
541
  getRegion(id) {
337
542
  return this.regions.get(id);
338
543
  }
@@ -350,10 +555,12 @@ class WorldBuildingSystem {
350
555
  }
351
556
  simulateYears(years) {
352
557
  const newEvents = [];
558
+ const regions = Array.from(this.regions.values());
559
+ const factions = Array.from(this.factions.values());
353
560
  for (let i = 0; i < years; i++) {
354
561
  this.currentYear++;
355
- if (Math.random() > 0.8) {
356
- const event = this.generateHistoricalEvent(this.currentYear, Array.from(this.regions.values()), Array.from(this.factions.values()));
562
+ if (this.rng.bool({ likelihood: 20 })) {
563
+ const event = this.generateHistoricalEvent(this.currentYear, regions, factions);
357
564
  newEvents.push(event);
358
565
  this.historicalEvents.push(event);
359
566
  this.applyHistoricalConsequences(event);
@@ -398,16 +605,12 @@ class WorldBuildingSystem {
398
605
  if (!faction)
399
606
  return 0;
400
607
  let influence = faction.influence;
401
- // Add influence from allies
402
- const allies = this.getFactionAllies(factionId);
403
- allies.forEach(allyId => {
608
+ this.getFactionAllies(factionId).forEach(allyId => {
404
609
  const ally = this.factions.get(allyId);
405
610
  if (ally)
406
611
  influence += ally.influence * 0.3;
407
612
  });
408
- // Subtract influence from enemies
409
- const enemies = this.getFactionEnemies(factionId);
410
- enemies.forEach(enemyId => {
613
+ this.getFactionEnemies(factionId).forEach(enemyId => {
411
614
  const enemy = this.factions.get(enemyId);
412
615
  if (enemy)
413
616
  influence -= enemy.influence * 0.2;
@@ -419,30 +622,32 @@ class WorldBuildingSystem {
419
622
  if (!faction)
420
623
  return [];
421
624
  const routes = [];
625
+ const types = ['goods', 'resources', 'magic', 'information'];
422
626
  Object.entries(faction.relationships).forEach(([partnerId, relationship]) => {
423
627
  if (relationship > 0) {
424
628
  const partner = this.factions.get(partnerId);
425
629
  if (partner) {
426
- const volume = Math.floor((relationship + 5) * 10);
427
- const types = ['goods', 'resources', 'magic', 'information'];
428
- const type = types[Math.floor(Math.random() * types.length)];
429
- routes.push({ partner: partner.name, volume, type });
630
+ routes.push({
631
+ partner: partner.name,
632
+ volume: Math.floor((relationship + 5) * 10),
633
+ type: this.rng.pickone(types)
634
+ });
430
635
  }
431
636
  }
432
637
  });
433
638
  return routes;
434
639
  }
435
640
  getFactionPowerRanking() {
436
- const rankings = Array.from(this.factions.values()).map(faction => ({
641
+ return Array.from(this.factions.values())
642
+ .map(faction => ({
437
643
  factionId: faction.id,
438
644
  name: faction.name,
439
645
  power: this.calculateFactionInfluence(faction.id) + faction.resources.gold * 0.01
440
- }));
441
- return rankings.sort((a, b) => b.power - a.power);
646
+ }))
647
+ .sort((a, b) => b.power - a.power);
442
648
  }
443
649
  getFactionGoals(factionId) {
444
- const faction = this.factions.get(factionId);
445
- return faction?.goals || [];
650
+ return this.factions.get(factionId)?.goals ?? [];
446
651
  }
447
652
  updateFactionRelationship(factionId1, factionId2, change) {
448
653
  const faction1 = this.factions.get(factionId1);
@@ -459,17 +664,23 @@ class WorldBuildingSystem {
459
664
  if (!faction)
460
665
  return null;
461
666
  const network = {
462
- faction: faction,
667
+ faction,
463
668
  allies: [],
464
669
  enemies: [],
465
670
  neutrals: []
466
671
  };
467
672
  if (depth > 0) {
468
- network.allies = this.getFactionAllies(factionId).map(id => this.getFactionNetwork(id, depth - 1)).filter(Boolean);
469
- network.enemies = this.getFactionEnemies(factionId).map(id => this.getFactionNetwork(id, depth - 1)).filter(Boolean);
470
- const allRelationships = Object.keys(faction.relationships);
471
- const alliesAndEnemies = new Set([...network.allies.map((f) => f.faction.id), ...network.enemies.map((f) => f.faction.id)]);
472
- network.neutrals = allRelationships
673
+ network.allies = this.getFactionAllies(factionId)
674
+ .map(id => this.getFactionNetwork(id, depth - 1))
675
+ .filter(Boolean);
676
+ network.enemies = this.getFactionEnemies(factionId)
677
+ .map(id => this.getFactionNetwork(id, depth - 1))
678
+ .filter(Boolean);
679
+ const alliesAndEnemies = new Set([
680
+ ...network.allies.map((f) => f.faction.id),
681
+ ...network.enemies.map((f) => f.faction.id)
682
+ ]);
683
+ network.neutrals = Object.keys(faction.relationships)
473
684
  .filter(id => !alliesAndEnemies.has(id))
474
685
  .map(id => this.factions.get(id))
475
686
  .filter(Boolean);
@@ -479,34 +690,32 @@ class WorldBuildingSystem {
479
690
  calculateEventSignificance(eventType, factionCount, regionCount) {
480
691
  let baseSignificance = 5;
481
692
  const typeMultipliers = {
482
- 'war': 8,
483
- 'alliance': 6,
484
- 'discovery': 7,
485
- 'disaster': 9,
486
- 'plague': 10,
487
- 'revolution': 9,
488
- 'invasion': 8,
489
- 'fall': 7,
490
- 'ascension': 7,
491
- 'treaty': 5,
492
- 'betrayal': 6,
493
- 'famine': 7
693
+ war: 8,
694
+ alliance: 6,
695
+ discovery: 7,
696
+ disaster: 9,
697
+ plague: 10,
698
+ revolution: 9,
699
+ invasion: 8,
700
+ fall: 7,
701
+ ascension: 7,
702
+ treaty: 5,
703
+ betrayal: 6,
704
+ famine: 7
494
705
  };
495
706
  baseSignificance *= typeMultipliers[eventType] || 5;
496
- baseSignificance *= (1 + (factionCount - 1) * 0.5);
497
- baseSignificance *= (1 + (regionCount - 1) * 0.3);
498
- return Math.min(10, Math.max(1, baseSignificance + (Math.random() - 0.5) * 2));
707
+ baseSignificance *= 1 + (factionCount - 1) * 0.5;
708
+ baseSignificance *= 1 + (regionCount - 1) * 0.3;
709
+ return Math.min(10, Math.max(1, baseSignificance + this.rng.floating({ min: -1, max: 1 })));
499
710
  }
500
711
  getRegionResources(regionId) {
501
- const region = this.regions.get(regionId);
502
- return region?.resources || [];
712
+ return this.regions.get(regionId)?.resources ?? [];
503
713
  }
504
714
  getWorldStats() {
505
715
  const regions = Array.from(this.regions.values());
506
- const factions = Array.from(this.factions.values());
507
716
  return {
508
717
  totalRegions: regions.length,
509
- totalFactions: factions.length,
718
+ totalFactions: this.factions.size,
510
719
  totalHistoricalEvents: this.historicalEvents.length,
511
720
  averageStability: regions.reduce((sum, r) => sum + r.political_stability, 0) / regions.length,
512
721
  averageProsperity: regions.reduce((sum, r) => sum + r.economic_prosperity, 0) / regions.length