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.
- package/README.md +48 -371
- package/demo.js +75 -1055
- package/dist/RPGEventGenerator.d.ts +24 -2
- package/dist/RPGEventGenerator.d.ts.map +1 -1
- package/dist/RPGEventGenerator.js +45 -19
- package/dist/RPGEventGenerator.js.map +1 -1
- package/dist/chains/ChainSystem.js +1 -1
- package/dist/chains/index.js +1 -1
- package/dist/core/ContextAnalyzer.js +1 -1
- package/dist/core/DifficultyScaler.js +1 -1
- package/dist/core/GeneratorCore.d.ts +1 -41
- package/dist/core/GeneratorCore.d.ts.map +1 -1
- package/dist/core/GeneratorCore.js +6 -48
- package/dist/core/GeneratorCore.js.map +1 -1
- package/dist/core/MarkovEngine.js +1 -1
- package/dist/core/index.js +1 -1
- package/dist/database/MemoryDatabaseAdapter.js +1 -1
- package/dist/database/TemplateDatabase.js +1 -1
- package/dist/environment/EnvironmentalSystem.d.ts +2 -0
- package/dist/environment/EnvironmentalSystem.d.ts.map +1 -1
- package/dist/environment/EnvironmentalSystem.js +46 -10
- package/dist/environment/EnvironmentalSystem.js.map +1 -1
- package/dist/environment/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/interfaces/systems.d.ts +1 -1
- package/dist/interfaces/systems.d.ts.map +1 -1
- package/dist/localization/LocalizationSystem.js +1 -1
- package/dist/localization/index.js +1 -1
- package/dist/relationships/RelationshipSystem.js +1 -1
- package/dist/relationships/index.js +1 -1
- package/dist/rules/RuleEngine.js +1 -1
- package/dist/rules/index.js +1 -1
- package/dist/src/types/world.d.ts +9 -0
- package/dist/src/types/world.d.ts.map +1 -1
- package/dist/templates/TemplateSystem.js +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/time/TimeSystem.js +1 -1
- package/dist/time/index.js +1 -1
- package/dist/types/world.d.ts +9 -0
- package/dist/types/world.d.ts.map +1 -1
- package/dist/utils/array.js +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/file.js +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/random.js +1 -1
- package/dist/utils/text.js +1 -1
- package/dist/utils/validation.js +1 -1
- package/dist/utils/version.d.ts +8 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +11 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/world/WorldBuildingSystem.d.ts +16 -2
- package/dist/world/WorldBuildingSystem.d.ts.map +1 -1
- package/dist/world/WorldBuildingSystem.js +357 -155
- package/dist/world/WorldBuildingSystem.js.map +1 -1
- package/dist/world/index.d.ts +1 -1
- package/dist/world/index.d.ts.map +1 -1
- package/dist/world/index.js.map +1 -1
- package/dist/world/worldContent.d.ts +13 -0
- package/dist/world/worldContent.d.ts.map +1 -0
- package/dist/world/worldContent.js +109 -0
- package/dist/world/worldContent.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// RPG Event Generator
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
const
|
|
23
|
-
|
|
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:
|
|
32
|
-
culture
|
|
33
|
-
climate
|
|
34
|
-
political_stability:
|
|
35
|
-
economic_prosperity:
|
|
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
|
-
|
|
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:
|
|
40
|
-
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
|
|
45
|
-
resources: this.generateResources(
|
|
46
|
-
population:
|
|
47
|
-
culture
|
|
48
|
-
climate
|
|
49
|
-
political_stability: Math.max(0.1, continent.political_stability +
|
|
50
|
-
economic_prosperity: Math.max(0.1, continent.economic_prosperity +
|
|
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 = [
|
|
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
|
|
66
|
-
name:
|
|
67
|
-
type
|
|
68
|
-
significance:
|
|
69
|
-
description: `A
|
|
70
|
-
discovered:
|
|
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
|
|
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
|
|
81
|
-
abundance:
|
|
82
|
-
quality:
|
|
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 = [
|
|
90
|
-
|
|
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:
|
|
94
|
-
type
|
|
95
|
-
leader:
|
|
144
|
+
name: this.generateFactionName(type, kingdom),
|
|
145
|
+
type,
|
|
146
|
+
leader: this.generateLeaderName(kingdom.culture, kingdom.name),
|
|
96
147
|
home_region: kingdom.id,
|
|
97
|
-
influence:
|
|
98
|
-
reputation:
|
|
148
|
+
influence: this.chance.floating({ min: 0, max: 10 }),
|
|
149
|
+
reputation: this.chance.floating({ min: -5, max: 5 }),
|
|
99
150
|
resources: {
|
|
100
|
-
gold:
|
|
101
|
-
influence:
|
|
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: [
|
|
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(
|
|
110
|
-
if (faction.id
|
|
111
|
-
|
|
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
|
-
|
|
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 =
|
|
121
|
-
if (
|
|
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 = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 =>
|
|
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}_${
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
const
|
|
173
|
-
const
|
|
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':
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
case 'discovery':
|
|
180
|
-
|
|
181
|
-
case '
|
|
182
|
-
|
|
183
|
-
case '
|
|
184
|
-
|
|
185
|
-
case '
|
|
186
|
-
|
|
187
|
-
case '
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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,
|
|
337
|
+
generateAdvancedConsequences(eventType, factions, regionIds, allRegions) {
|
|
199
338
|
const consequences = [];
|
|
200
339
|
factions.forEach(faction => {
|
|
201
|
-
const baseChange =
|
|
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 +
|
|
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 +
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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 (
|
|
356
|
-
const event = this.generateHistoricalEvent(this.currentYear,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
639
|
+
}))
|
|
640
|
+
.sort((a, b) => b.power - a.power);
|
|
442
641
|
}
|
|
443
642
|
getFactionGoals(factionId) {
|
|
444
|
-
|
|
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
|
|
660
|
+
faction,
|
|
463
661
|
allies: [],
|
|
464
662
|
enemies: [],
|
|
465
663
|
neutrals: []
|
|
466
664
|
};
|
|
467
665
|
if (depth > 0) {
|
|
468
|
-
network.allies = this.getFactionAllies(factionId)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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 *=
|
|
497
|
-
baseSignificance *=
|
|
498
|
-
return Math.min(10, Math.max(1, baseSignificance +
|
|
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
|
-
|
|
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.
|
|
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
|