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.
- package/README.md +48 -371
- package/demo.js +80 -1055
- package/dist/RPGEventGenerator.d.ts +25 -3
- package/dist/RPGEventGenerator.d.ts.map +1 -1
- package/dist/RPGEventGenerator.js +52 -26
- package/dist/RPGEventGenerator.js.map +1 -1
- package/dist/chains/ChainSystem.d.ts +3 -2
- package/dist/chains/ChainSystem.d.ts.map +1 -1
- package/dist/chains/ChainSystem.js +5 -5
- package/dist/chains/ChainSystem.js.map +1 -1
- package/dist/chains/index.js +1 -1
- package/dist/core/ContextAnalyzer.js +1 -1
- package/dist/core/DescriptionFragmentLibrary.d.ts +3 -2
- package/dist/core/DescriptionFragmentLibrary.d.ts.map +1 -1
- package/dist/core/DescriptionFragmentLibrary.js +4 -4
- package/dist/core/DescriptionFragmentLibrary.js.map +1 -1
- package/dist/core/DifficultyScaler.js +1 -1
- package/dist/core/GeneratorCore.d.ts +4 -43
- package/dist/core/GeneratorCore.d.ts.map +1 -1
- package/dist/core/GeneratorCore.js +29 -71
- package/dist/core/GeneratorCore.js.map +1 -1
- package/dist/core/GrammarRulesEngine.d.ts +3 -2
- package/dist/core/GrammarRulesEngine.d.ts.map +1 -1
- package/dist/core/GrammarRulesEngine.js +4 -4
- package/dist/core/GrammarRulesEngine.js.map +1 -1
- package/dist/core/MarkovEngine.js +1 -1
- package/dist/core/SentenceBuilder.d.ts +3 -2
- package/dist/core/SentenceBuilder.d.ts.map +1 -1
- package/dist/core/SentenceBuilder.js +7 -36
- package/dist/core/SentenceBuilder.js.map +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 +5 -2
- package/dist/environment/EnvironmentalSystem.d.ts.map +1 -1
- package/dist/environment/EnvironmentalSystem.js +50 -14
- 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/config.d.ts +3 -1
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/world.d.ts +9 -0
- package/dist/src/types/world.d.ts.map +1 -1
- package/dist/src/utils/random.d.ts +73 -0
- package/dist/src/utils/random.d.ts.map +1 -0
- package/dist/src/utils/random.js +182 -0
- package/dist/src/utils/random.js.map +1 -0
- package/dist/templates/TemplateSystem.js +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/time/TimeSystem.d.ts +3 -2
- package/dist/time/TimeSystem.d.ts.map +1 -1
- package/dist/time/TimeSystem.js +7 -7
- package/dist/time/TimeSystem.js.map +1 -1
- package/dist/time/index.js +1 -1
- package/dist/types/config.d.ts +3 -1
- package/dist/types/config.d.ts.map +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.d.ts +23 -2
- package/dist/utils/random.d.ts.map +1 -1
- package/dist/utils/random.js +62 -5
- package/dist/utils/random.js.map +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 +364 -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 -5
|
@@ -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 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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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.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:
|
|
32
|
-
culture
|
|
33
|
-
climate
|
|
34
|
-
political_stability:
|
|
35
|
-
economic_prosperity:
|
|
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
|
-
|
|
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:
|
|
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.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 = [
|
|
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
|
|
66
|
-
name:
|
|
67
|
-
type
|
|
68
|
-
significance:
|
|
69
|
-
description: `A
|
|
70
|
-
discovered:
|
|
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
|
|
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
|
|
81
|
-
abundance:
|
|
82
|
-
quality:
|
|
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 = [
|
|
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.rng.floating({ min: 0, max: 10 }),
|
|
149
|
+
reputation: this.rng.floating({ min: -5, max: 5 }),
|
|
99
150
|
resources: {
|
|
100
|
-
gold:
|
|
101
|
-
influence:
|
|
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: [
|
|
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(
|
|
110
|
-
if (faction.id
|
|
111
|
-
|
|
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
|
-
|
|
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.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 = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 =>
|
|
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}_${
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
const
|
|
173
|
-
const
|
|
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':
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
case 'discovery':
|
|
180
|
-
|
|
181
|
-
case '
|
|
182
|
-
|
|
183
|
-
case '
|
|
184
|
-
|
|
185
|
-
case '
|
|
186
|
-
|
|
187
|
-
case '
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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,
|
|
344
|
+
generateAdvancedConsequences(eventType, factions, regionIds, allRegions) {
|
|
199
345
|
const consequences = [];
|
|
200
346
|
factions.forEach(faction => {
|
|
201
|
-
const baseChange =
|
|
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 +
|
|
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 +
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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 (
|
|
356
|
-
const event = this.generateHistoricalEvent(this.currentYear,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
+
}))
|
|
647
|
+
.sort((a, b) => b.power - a.power);
|
|
442
648
|
}
|
|
443
649
|
getFactionGoals(factionId) {
|
|
444
|
-
|
|
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
|
|
667
|
+
faction,
|
|
463
668
|
allies: [],
|
|
464
669
|
enemies: [],
|
|
465
670
|
neutrals: []
|
|
466
671
|
};
|
|
467
672
|
if (depth > 0) {
|
|
468
|
-
network.allies = this.getFactionAllies(factionId)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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 *=
|
|
497
|
-
baseSignificance *=
|
|
498
|
-
return Math.min(10, Math.max(1, baseSignificance +
|
|
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
|
-
|
|
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.
|
|
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
|