rpg-event-generator 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +48 -371
  2. package/demo.js +75 -1055
  3. package/dist/RPGEventGenerator.d.ts +24 -2
  4. package/dist/RPGEventGenerator.d.ts.map +1 -1
  5. package/dist/RPGEventGenerator.js +45 -19
  6. package/dist/RPGEventGenerator.js.map +1 -1
  7. package/dist/chains/ChainSystem.js +1 -1
  8. package/dist/chains/index.js +1 -1
  9. package/dist/core/ContextAnalyzer.js +1 -1
  10. package/dist/core/DifficultyScaler.js +1 -1
  11. package/dist/core/GeneratorCore.d.ts +1 -41
  12. package/dist/core/GeneratorCore.d.ts.map +1 -1
  13. package/dist/core/GeneratorCore.js +6 -48
  14. package/dist/core/GeneratorCore.js.map +1 -1
  15. package/dist/core/MarkovEngine.js +1 -1
  16. package/dist/core/index.js +1 -1
  17. package/dist/database/MemoryDatabaseAdapter.js +1 -1
  18. package/dist/database/TemplateDatabase.js +1 -1
  19. package/dist/environment/EnvironmentalSystem.d.ts +2 -0
  20. package/dist/environment/EnvironmentalSystem.d.ts.map +1 -1
  21. package/dist/environment/EnvironmentalSystem.js +46 -10
  22. package/dist/environment/EnvironmentalSystem.js.map +1 -1
  23. package/dist/environment/index.js +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/interfaces/systems.d.ts +1 -1
  26. package/dist/interfaces/systems.d.ts.map +1 -1
  27. package/dist/localization/LocalizationSystem.js +1 -1
  28. package/dist/localization/index.js +1 -1
  29. package/dist/relationships/RelationshipSystem.js +1 -1
  30. package/dist/relationships/index.js +1 -1
  31. package/dist/rules/RuleEngine.js +1 -1
  32. package/dist/rules/index.js +1 -1
  33. package/dist/src/types/world.d.ts +9 -0
  34. package/dist/src/types/world.d.ts.map +1 -1
  35. package/dist/templates/TemplateSystem.js +1 -1
  36. package/dist/templates/index.js +1 -1
  37. package/dist/time/TimeSystem.js +1 -1
  38. package/dist/time/index.js +1 -1
  39. package/dist/types/world.d.ts +9 -0
  40. package/dist/types/world.d.ts.map +1 -1
  41. package/dist/utils/array.js +1 -1
  42. package/dist/utils/constants.js +1 -1
  43. package/dist/utils/file.js +1 -1
  44. package/dist/utils/index.d.ts +1 -0
  45. package/dist/utils/index.d.ts.map +1 -1
  46. package/dist/utils/index.js +2 -1
  47. package/dist/utils/index.js.map +1 -1
  48. package/dist/utils/random.js +1 -1
  49. package/dist/utils/text.js +1 -1
  50. package/dist/utils/validation.js +1 -1
  51. package/dist/utils/version.d.ts +8 -0
  52. package/dist/utils/version.d.ts.map +1 -0
  53. package/dist/utils/version.js +11 -0
  54. package/dist/utils/version.js.map +1 -0
  55. package/dist/world/WorldBuildingSystem.d.ts +16 -2
  56. package/dist/world/WorldBuildingSystem.d.ts.map +1 -1
  57. package/dist/world/WorldBuildingSystem.js +357 -155
  58. package/dist/world/WorldBuildingSystem.js.map +1 -1
  59. package/dist/world/index.d.ts +1 -1
  60. package/dist/world/index.d.ts.map +1 -1
  61. package/dist/world/index.js.map +1 -1
  62. package/dist/world/worldContent.d.ts +13 -0
  63. package/dist/world/worldContent.d.ts.map +1 -0
  64. package/dist/world/worldContent.js +109 -0
  65. package/dist/world/worldContent.js.map +1 -0
  66. package/package.json +1 -1
@@ -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 chance_1 = require("chance");
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.chance = new chance_1.Chance();
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.chance = options.seed !== undefined ? new chance_1.Chance(options.seed) : new chance_1.Chance();
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.chance.pickset(worldContent_1.CONTINENT_NAMES, count);
43
+ continents.forEach((name, i) => {
44
+ const culture = this.chance.pickone(['Human', 'Elven', 'Dwarven', 'Orcish']);
45
+ const climate = this.chance.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.chance.integer({ min: 5000000, max: 14999999 }),
54
+ culture,
55
+ climate,
56
+ political_stability: this.chance.floating({ min: 0.2, max: 1.0 }),
57
+ economic_prosperity: this.chance.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.chance.integer({ min: 2, max: 4 });
60
+ const usedPrefixes = new Set();
61
+ for (let j = 0; j < kingdomCount; j++) {
62
+ let prefix = this.chance.pickone(worldContent_1.KINGDOM_PREFIXES);
63
+ while (usedPrefixes.has(prefix) && usedPrefixes.size < worldContent_1.KINGDOM_PREFIXES.length) {
64
+ prefix = this.chance.pickone(worldContent_1.KINGDOM_PREFIXES);
65
+ }
66
+ usedPrefixes.add(prefix);
67
+ const suffix = this.chance.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.chance.integer({ min: 2, max: 4 }), kingdomId, climate, culture),
76
+ resources: this.generateResources(this.chance.integer({ min: 3, max: 6 }), climate),
77
+ population: this.chance.integer({ min: 500000, max: 2499999 }),
78
+ culture,
79
+ climate,
80
+ political_stability: Math.max(0.1, continent.political_stability + this.chance.floating({ min: -0.2, max: 0.2 })),
81
+ economic_prosperity: Math.max(0.1, continent.economic_prosperity + this.chance.floating({ min: -0.2, max: 0.2 }))
51
82
  };
52
83
  continent.sub_regions.push(kingdom.id);
53
84
  regions.push(kingdom);
@@ -57,68 +88,128 @@ 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.chance.pickone(landmarkTypes);
98
+ const adjective = this.chance.pickone(worldContent_1.LANDMARK_ADJECTIVES);
99
+ const noun = this.chance.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.chance.floating({ min: 0, max: 10 }),
106
+ description: `A ${adjective.toLowerCase()} ${type} sacred to ${culture} folk in the ${climate} lands.`,
107
+ discovered: this.chance.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.chance.pickone(resourceTypes);
119
+ let attempts = 0;
120
+ while (used.has(type) && attempts < 10) {
121
+ type = this.chance.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.chance.floating({ min: 0, max: 10 }),
128
+ quality: this.chance.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.chance.floating({ min: 0, max: 10 }),
149
+ reputation: this.chance.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.chance.integer({ min: 0, max: 9999 }),
152
+ influence: this.chance.integer({ min: 0, max: 99 })
102
153
  },
103
154
  relationships: {},
104
- goals: ['Expand territory', 'Increase wealth', 'Gain political power']
155
+ goals: this.chance.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.chance.pickone(worldContent_1.LANDMARK_ADJECTIVES)} ${this.chance.pickone(['Smiths', 'Arcane', 'Shipwrights', 'Healers'])} Guild`;
168
+ case 'cult':
169
+ return `Cult of ${this.chance.pickone(worldContent_1.CULT_DEITIES)}`;
170
+ case 'tribe':
171
+ return `${shortName} ${this.chance.pickone(['Clans', 'Horde', 'Kin', 'People'])}`;
172
+ case 'merchants':
173
+ return `${shortName} Merchant League`;
174
+ case 'nobles':
175
+ return `House ${this.chance.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.chance.pickone(titles);
183
+ const name = this.chance.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.chance.floating({ min: -5, max: 5 });
192
+ if (this.areNeighborFactions(faction, other, regions)) {
193
+ relationship = this.chance.bool({ likelihood: 45 })
194
+ ? this.chance.floating({ min: -5, max: -0.5 })
195
+ : this.chance.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.chance.integer({ min: 0, max: 99 })) {
212
+ if (this.chance.bool({ likelihood: 30 })) {
122
213
  const event = this.generateHistoricalEvent(year, regions, factions);
123
214
  events.push(event);
124
215
  this.applyHistoricalConsequences(event);
@@ -127,78 +218,126 @@ class WorldBuildingSystem {
127
218
  return events;
128
219
  }
129
220
  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);
221
+ const eventTypes = [
222
+ 'war', 'alliance', 'discovery', 'disaster', 'ascension', 'fall',
223
+ 'plague', 'famine', 'revolution', 'invasion', 'treaty', 'betrayal'
224
+ ];
225
+ const eventType = this.chance.pickone(eventTypes);
226
+ const involvedFactions = this.pickFactionsForEvent(eventType, factions);
136
227
  const affectedRegions = regions
137
- .filter(r => involvedFactions.some(f => f.home_region === r.id || r.sub_regions.includes(f.home_region)))
228
+ .filter(r => r.type === 'kingdom' &&
229
+ involvedFactions.some(f => f.home_region === r.id))
138
230
  .map(r => r.id);
231
+ const primaryRegion = regions.find(r => r.id === involvedFactions[0]?.home_region);
139
232
  const event = {
140
- id: `event_${year}_${Math.random().toString(36).substr(2, 9)}`,
233
+ id: `event_${year}_${this.chance.string({ length: 9, pool: 'abcdefghijklmnopqrstuvwxyz0123456789' })}`,
141
234
  year,
142
- title: this.generateEventTitle(eventType, involvedFactions),
143
- description: this.generateEventDescription(eventType, involvedFactions),
235
+ title: this.generateEventTitle(eventType, involvedFactions, primaryRegion),
236
+ description: this.generateEventDescription(eventType, involvedFactions, primaryRegion),
144
237
  type: eventType,
145
- regions_affected: affectedRegions,
238
+ regions_affected: affectedRegions.length > 0 ? affectedRegions : involvedFactions.map(f => f.home_region),
146
239
  factions_involved: involvedFactions.map(f => f.id),
147
- consequences: this.generateAdvancedConsequences(eventType, involvedFactions, affectedRegions, regions),
148
- significance: this.calculateEventSignificance(eventType, involvedFactions.length, affectedRegions.length)
240
+ consequences: this.generateAdvancedConsequences(eventType, involvedFactions, affectedRegions.length > 0 ? affectedRegions : involvedFactions.map(f => f.home_region), regions),
241
+ significance: this.calculateEventSignificance(eventType, involvedFactions.length, Math.max(1, affectedRegions.length))
149
242
  };
150
243
  return event;
151
244
  }
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`;
245
+ pickFactionsForEvent(eventType, factions) {
246
+ if (factions.length === 0)
247
+ return [];
248
+ if (factions.length === 1)
249
+ return [factions[0]];
250
+ const shuffled = this.chance.shuffle([...factions]);
251
+ const pairTypes = [
252
+ 'war', 'alliance', 'betrayal', 'treaty', 'invasion'
253
+ ];
254
+ if (!pairTypes.includes(eventType)) {
255
+ return [this.chance.pickone(shuffled)];
256
+ }
257
+ if (['war', 'invasion', 'betrayal'].includes(eventType)) {
258
+ for (const faction of shuffled) {
259
+ const enemies = Object.entries(faction.relationships)
260
+ .filter(([_, value]) => value < -1)
261
+ .map(([id]) => this.factions.get(id))
262
+ .filter((f) => !!f);
263
+ if (enemies.length > 0) {
264
+ return [faction, this.chance.pickone(enemies)];
265
+ }
266
+ }
267
+ }
268
+ if (['alliance', 'treaty'].includes(eventType)) {
269
+ for (const faction of shuffled) {
270
+ const allies = Object.entries(faction.relationships)
271
+ .filter(([_, value]) => value > 1)
272
+ .map(([id]) => this.factions.get(id))
273
+ .filter((f) => !!f);
274
+ if (allies.length > 0) {
275
+ return [faction, this.chance.pickone(allies)];
276
+ }
277
+ }
169
278
  }
279
+ return shuffled.slice(0, 2);
170
280
  }
171
- generateEventDescription(type, factions) {
172
- const factionName = factions[0]?.name || 'Unknown';
173
- const secondaryFaction = factions[1]?.name || 'Unknown';
281
+ generateEventTitle(type, factions, region) {
282
+ const place = region?.name ?? factions[0]?.name ?? 'Unknown';
283
+ const factionA = factions[0]?.name ?? 'Unknown';
284
+ const factionB = factions[1]?.name ?? 'Unknown';
174
285
  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.`;
286
+ case 'war':
287
+ return factions.length > 1 ? `War of ${place}` : `${factionA} Conquest`;
288
+ case 'alliance':
289
+ return `Pact of ${place}`;
290
+ case 'discovery':
291
+ return `Discovery at ${place}`;
292
+ case 'disaster':
293
+ return `Calamity in ${place}`;
294
+ case 'ascension':
295
+ return `Rise of ${factionA}`;
296
+ case 'fall':
297
+ return `Fall of ${factionA}`;
298
+ case 'plague':
299
+ return `Plague of ${place}`;
300
+ case 'famine':
301
+ return `Famine in ${place}`;
302
+ case 'revolution':
303
+ return `Revolt in ${place}`;
304
+ case 'invasion':
305
+ return `Invasion of ${place}`;
306
+ case 'treaty':
307
+ return `Treaty of ${place}`;
308
+ case 'betrayal':
309
+ return factions.length > 1 ? `${factionA} Betrays ${factionB}` : `Betrayal at ${place}`;
310
+ default:
311
+ return `${place} Turning Point`;
312
+ }
313
+ }
314
+ generateEventDescription(type, factions, region) {
315
+ const templates = worldContent_1.HISTORICAL_TEMPLATES[type];
316
+ const template = templates ? this.chance.pickone(templates) : null;
317
+ const landmark = region?.landmarks?.length
318
+ ? this.chance.pickone(region.landmarks).name
319
+ : 'a border fort';
320
+ const vars = {
321
+ factionA: factions[0]?.name ?? 'Unknown',
322
+ factionB: factions[1]?.name ?? 'a rival power',
323
+ region: region?.name ?? 'the borderlands',
324
+ landmark,
325
+ climate: region?.climate ?? 'temperate',
326
+ culture: region?.culture ?? 'Human'
327
+ };
328
+ if (template) {
329
+ return (0, worldContent_1.formatTemplate)(template, vars);
192
330
  }
331
+ return `${vars.factionA} shaped the fate of ${vars.region} near ${vars.landmark}.`;
193
332
  }
194
333
  generateConsequences(type, factions, regions) {
195
334
  const allRegions = Array.from(this.regions.values());
196
335
  return this.generateAdvancedConsequences(type, factions, regions, allRegions);
197
336
  }
198
- generateAdvancedConsequences(eventType, factions, regions, allRegions) {
337
+ generateAdvancedConsequences(eventType, factions, regionIds, allRegions) {
199
338
  const consequences = [];
200
339
  factions.forEach(faction => {
201
- const baseChange = Math.random() - 0.5;
340
+ const baseChange = this.chance.floating({ min: -0.5, max: 0.5 });
202
341
  switch (eventType) {
203
342
  case 'war':
204
343
  consequences.push({
@@ -238,7 +377,7 @@ class WorldBuildingSystem {
238
377
  target_id: faction.id,
239
378
  property: 'resources.gold',
240
379
  old_value: faction.resources.gold,
241
- new_value: faction.resources.gold + Math.floor(Math.random() * 10000)
380
+ new_value: faction.resources.gold + this.chance.integer({ min: 0, max: 9999 })
242
381
  });
243
382
  consequences.push({
244
383
  type: 'faction_change',
@@ -250,10 +389,10 @@ class WorldBuildingSystem {
250
389
  break;
251
390
  case 'disaster':
252
391
  case 'plague':
253
- case 'famine':
392
+ case 'famine': {
254
393
  const populationRegion = allRegions.find(r => r.id === faction.home_region);
255
394
  if (populationRegion) {
256
- const populationLoss = Math.floor(populationRegion.population * (0.05 + Math.random() * 0.15));
395
+ const populationLoss = Math.floor(populationRegion.population * (0.05 + this.chance.floating({ min: 0, max: 0.15 })));
257
396
  consequences.push({
258
397
  type: 'region_change',
259
398
  target_id: faction.home_region,
@@ -270,6 +409,7 @@ class WorldBuildingSystem {
270
409
  new_value: Math.max(0, faction.influence - Math.abs(baseChange) * 3)
271
410
  });
272
411
  break;
412
+ }
273
413
  case 'revolution':
274
414
  consequences.push({
275
415
  type: 'faction_change',
@@ -290,9 +430,8 @@ class WorldBuildingSystem {
290
430
  break;
291
431
  }
292
432
  });
293
- // Add regional consequences for certain events
294
433
  if (['disaster', 'plague', 'famine', 'invasion'].includes(eventType)) {
295
- regions.forEach(regionId => {
434
+ regionIds.forEach(regionId => {
296
435
  const region = allRegions.find(r => r.id === regionId);
297
436
  if (region) {
298
437
  consequences.push({
@@ -300,17 +439,37 @@ class WorldBuildingSystem {
300
439
  target_id: regionId,
301
440
  property: 'political_stability',
302
441
  old_value: region.political_stability,
303
- new_value: Math.max(0.1, region.political_stability - Math.random() * 0.3)
442
+ new_value: Math.max(0.1, region.political_stability - this.chance.floating({ min: 0, max: 0.3 }))
304
443
  });
305
444
  }
306
445
  });
307
446
  }
447
+ if (['alliance', 'treaty'].includes(eventType) && factions.length > 1) {
448
+ const [a, b] = factions;
449
+ consequences.push({
450
+ type: 'relationship_change',
451
+ target_id: a.id,
452
+ property: b.id,
453
+ old_value: a.relationships[b.id],
454
+ new_value: Math.min(10, (a.relationships[b.id] ?? 0) + 2)
455
+ });
456
+ }
457
+ if (['war', 'betrayal', 'invasion'].includes(eventType) && factions.length > 1) {
458
+ const [a, b] = factions;
459
+ consequences.push({
460
+ type: 'relationship_change',
461
+ target_id: a.id,
462
+ property: b.id,
463
+ old_value: a.relationships[b.id],
464
+ new_value: Math.max(-10, (a.relationships[b.id] ?? 0) - 2)
465
+ });
466
+ }
308
467
  return consequences;
309
468
  }
310
469
  applyHistoricalConsequences(event) {
311
470
  event.consequences.forEach(consequence => {
312
471
  switch (consequence.type) {
313
- case 'faction_change':
472
+ case 'faction_change': {
314
473
  const faction = this.factions.get(consequence.target_id);
315
474
  if (faction) {
316
475
  if (consequence.property.includes('.')) {
@@ -324,15 +483,54 @@ class WorldBuildingSystem {
324
483
  }
325
484
  }
326
485
  break;
327
- case 'region_change':
486
+ }
487
+ case 'region_change': {
328
488
  const region = this.regions.get(consequence.target_id);
329
489
  if (region) {
330
490
  region[consequence.property] = consequence.new_value;
331
491
  }
332
492
  break;
493
+ }
494
+ case 'relationship_change': {
495
+ const faction = this.factions.get(consequence.target_id);
496
+ const partner = this.factions.get(consequence.property);
497
+ if (faction && partner) {
498
+ faction.relationships[partner.id] = consequence.new_value;
499
+ partner.relationships[faction.id] = consequence.new_value;
500
+ }
501
+ break;
502
+ }
333
503
  }
334
504
  });
335
505
  }
506
+ /** Find a kingdom or region whose name matches the given location string. */
507
+ findRegionByLocation(location) {
508
+ if (!location)
509
+ return undefined;
510
+ const query = location.toLowerCase();
511
+ return Array.from(this.regions.values()).find(r => r.name.toLowerCase().includes(query) || r.id.toLowerCase() === query);
512
+ }
513
+ /** One-sentence lore hook for a player location — useful when layering world context onto events. */
514
+ getLoreSnippet(location) {
515
+ const region = this.findRegionByLocation(location);
516
+ if (!region)
517
+ return null;
518
+ const regionalEvents = this.historicalEvents
519
+ .filter(e => e.regions_affected.includes(region.id))
520
+ .sort((a, b) => b.year - a.year);
521
+ if (regionalEvents.length === 0) {
522
+ const faction = Array.from(this.factions.values()).find(f => f.home_region === region.id);
523
+ if (faction) {
524
+ if (faction.name === region.name) {
525
+ return `${region.name} is ruled by ${faction.leader}.`;
526
+ }
527
+ return `${region.name} is held by ${faction.name}, led by ${faction.leader}.`;
528
+ }
529
+ return `${region.name} is a ${region.climate} ${region.type} of the ${region.culture} peoples.`;
530
+ }
531
+ const latest = regionalEvents[0];
532
+ return `In ${latest.year}, ${latest.title.toLowerCase()} — ${latest.description}`;
533
+ }
336
534
  getRegion(id) {
337
535
  return this.regions.get(id);
338
536
  }
@@ -350,10 +548,12 @@ class WorldBuildingSystem {
350
548
  }
351
549
  simulateYears(years) {
352
550
  const newEvents = [];
551
+ const regions = Array.from(this.regions.values());
552
+ const factions = Array.from(this.factions.values());
353
553
  for (let i = 0; i < years; i++) {
354
554
  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()));
555
+ if (this.chance.bool({ likelihood: 20 })) {
556
+ const event = this.generateHistoricalEvent(this.currentYear, regions, factions);
357
557
  newEvents.push(event);
358
558
  this.historicalEvents.push(event);
359
559
  this.applyHistoricalConsequences(event);
@@ -398,16 +598,12 @@ class WorldBuildingSystem {
398
598
  if (!faction)
399
599
  return 0;
400
600
  let influence = faction.influence;
401
- // Add influence from allies
402
- const allies = this.getFactionAllies(factionId);
403
- allies.forEach(allyId => {
601
+ this.getFactionAllies(factionId).forEach(allyId => {
404
602
  const ally = this.factions.get(allyId);
405
603
  if (ally)
406
604
  influence += ally.influence * 0.3;
407
605
  });
408
- // Subtract influence from enemies
409
- const enemies = this.getFactionEnemies(factionId);
410
- enemies.forEach(enemyId => {
606
+ this.getFactionEnemies(factionId).forEach(enemyId => {
411
607
  const enemy = this.factions.get(enemyId);
412
608
  if (enemy)
413
609
  influence -= enemy.influence * 0.2;
@@ -419,30 +615,32 @@ class WorldBuildingSystem {
419
615
  if (!faction)
420
616
  return [];
421
617
  const routes = [];
618
+ const types = ['goods', 'resources', 'magic', 'information'];
422
619
  Object.entries(faction.relationships).forEach(([partnerId, relationship]) => {
423
620
  if (relationship > 0) {
424
621
  const partner = this.factions.get(partnerId);
425
622
  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 });
623
+ routes.push({
624
+ partner: partner.name,
625
+ volume: Math.floor((relationship + 5) * 10),
626
+ type: this.chance.pickone(types)
627
+ });
430
628
  }
431
629
  }
432
630
  });
433
631
  return routes;
434
632
  }
435
633
  getFactionPowerRanking() {
436
- const rankings = Array.from(this.factions.values()).map(faction => ({
634
+ return Array.from(this.factions.values())
635
+ .map(faction => ({
437
636
  factionId: faction.id,
438
637
  name: faction.name,
439
638
  power: this.calculateFactionInfluence(faction.id) + faction.resources.gold * 0.01
440
- }));
441
- return rankings.sort((a, b) => b.power - a.power);
639
+ }))
640
+ .sort((a, b) => b.power - a.power);
442
641
  }
443
642
  getFactionGoals(factionId) {
444
- const faction = this.factions.get(factionId);
445
- return faction?.goals || [];
643
+ return this.factions.get(factionId)?.goals ?? [];
446
644
  }
447
645
  updateFactionRelationship(factionId1, factionId2, change) {
448
646
  const faction1 = this.factions.get(factionId1);
@@ -459,17 +657,23 @@ class WorldBuildingSystem {
459
657
  if (!faction)
460
658
  return null;
461
659
  const network = {
462
- faction: faction,
660
+ faction,
463
661
  allies: [],
464
662
  enemies: [],
465
663
  neutrals: []
466
664
  };
467
665
  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
666
+ network.allies = this.getFactionAllies(factionId)
667
+ .map(id => this.getFactionNetwork(id, depth - 1))
668
+ .filter(Boolean);
669
+ network.enemies = this.getFactionEnemies(factionId)
670
+ .map(id => this.getFactionNetwork(id, depth - 1))
671
+ .filter(Boolean);
672
+ const alliesAndEnemies = new Set([
673
+ ...network.allies.map((f) => f.faction.id),
674
+ ...network.enemies.map((f) => f.faction.id)
675
+ ]);
676
+ network.neutrals = Object.keys(faction.relationships)
473
677
  .filter(id => !alliesAndEnemies.has(id))
474
678
  .map(id => this.factions.get(id))
475
679
  .filter(Boolean);
@@ -479,34 +683,32 @@ class WorldBuildingSystem {
479
683
  calculateEventSignificance(eventType, factionCount, regionCount) {
480
684
  let baseSignificance = 5;
481
685
  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
686
+ war: 8,
687
+ alliance: 6,
688
+ discovery: 7,
689
+ disaster: 9,
690
+ plague: 10,
691
+ revolution: 9,
692
+ invasion: 8,
693
+ fall: 7,
694
+ ascension: 7,
695
+ treaty: 5,
696
+ betrayal: 6,
697
+ famine: 7
494
698
  };
495
699
  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));
700
+ baseSignificance *= 1 + (factionCount - 1) * 0.5;
701
+ baseSignificance *= 1 + (regionCount - 1) * 0.3;
702
+ return Math.min(10, Math.max(1, baseSignificance + this.chance.floating({ min: -1, max: 1 })));
499
703
  }
500
704
  getRegionResources(regionId) {
501
- const region = this.regions.get(regionId);
502
- return region?.resources || [];
705
+ return this.regions.get(regionId)?.resources ?? [];
503
706
  }
504
707
  getWorldStats() {
505
708
  const regions = Array.from(this.regions.values());
506
- const factions = Array.from(this.factions.values());
507
709
  return {
508
710
  totalRegions: regions.length,
509
- totalFactions: factions.length,
711
+ totalFactions: this.factions.size,
510
712
  totalHistoricalEvents: this.historicalEvents.length,
511
713
  averageStability: regions.reduce((sum, r) => sum + r.political_stability, 0) / regions.length,
512
714
  averageProsperity: regions.reduce((sum, r) => sum + r.economic_prosperity, 0) / regions.length