ec8ae7e063
Signed-off-by: Xe Iaso <me@christine.website>
337 lines
8.8 KiB
JavaScript
337 lines
8.8 KiB
JavaScript
'use strict'
|
|
const seedrandom = require('seedrandom');
|
|
const plotto = require("./data/plotto.json");
|
|
|
|
function regEscape(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); };
|
|
|
|
//seedrandom doesn't expose the seed used, so generate one here
|
|
const defaultSeed = seedrandom()();
|
|
const defaultRng = seedrandom(defaultSeed);
|
|
|
|
const genderMap = {
|
|
"A": "male",
|
|
"A-2": "male",
|
|
"A-3": "male",
|
|
"A-4": "male",
|
|
"A-5": "male",
|
|
"A-6": "male",
|
|
"A-7": "male",
|
|
"A-8": "male",
|
|
"A-9": "male",
|
|
"B": "female",
|
|
"B-2": "female",
|
|
"B-3": "female",
|
|
"B-4": "female",
|
|
"B-5": "female",
|
|
"B-6": "female",
|
|
"B-7": "female",
|
|
"B-8": "female",
|
|
"B-9": "female",
|
|
"F-A": "father",
|
|
"M-A": "mother",
|
|
"BR-A": "male",
|
|
"SR-A": "female",
|
|
"SN-A": "male",
|
|
"D-A": "female",
|
|
"U-A": "male",
|
|
"AU-A": "female",
|
|
"CN-A": "male",
|
|
"NW-A": "male",
|
|
"NC-A": "female",
|
|
"GF-A": "male",
|
|
"GM-A": "female",
|
|
"SF-A": "male",
|
|
"SM-A": "female",
|
|
"GCH-A": "any",
|
|
"F-B": "male",
|
|
"M-B": "female",
|
|
"BR-B": "male",
|
|
"SR-B": "female",
|
|
"SN-B": "male",
|
|
"D-B": "female",
|
|
"U-B": "male",
|
|
"AU-B": "female",
|
|
"CN-B": "female",
|
|
"NW-B": "male",
|
|
"NC-B": "female",
|
|
"GF-B": "male",
|
|
"GM-B": "female",
|
|
"SF-B": "male",
|
|
"SM-B": "female",
|
|
"GCH-B": "any",
|
|
"BR": "male",
|
|
"SR": "female",
|
|
"SN": "male",
|
|
"D": "female",
|
|
"CN": "any",
|
|
"CH": "any",
|
|
"AX": "male",
|
|
"BX": "female",
|
|
"X": "none"
|
|
};
|
|
|
|
function createRandomPicker(rng = defaultRng) {
|
|
return function randomPicker(arr) {
|
|
if (!arr) {
|
|
return null;
|
|
}
|
|
if (arr.length == 0)
|
|
return null;
|
|
if (arr.length == 1)
|
|
return arr[0];
|
|
|
|
let min = 0;
|
|
let max = arr.length;
|
|
let i = Math.floor(rng() * (max - min)) + min;
|
|
let ret = arr[i];
|
|
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
let maleNames = [];
|
|
let femaleNames = [];
|
|
|
|
function createRandomNamer(rng = defaultRng) {
|
|
return function randomNamer(characterSymbol, symbolDescription, gender) {
|
|
|
|
let arr;
|
|
if (gender == 'male') {
|
|
arr = maleNames;
|
|
} else if (gender == 'female') {
|
|
arr = femaleNames;
|
|
} else if (gender == 'any') {
|
|
|
|
if (rng() < 0.5) {
|
|
arr = maleNames;
|
|
} else {
|
|
arr = femaleNames;
|
|
}
|
|
|
|
} else {
|
|
return characterSymbol;
|
|
}
|
|
|
|
let min = 0;
|
|
let max = arr.length;
|
|
let i = Math.floor(rng() * (max - min)) + min;
|
|
let ret = arr[i];
|
|
|
|
arr.splice(i, 1);
|
|
|
|
return ret || characterSymbol;
|
|
};
|
|
}
|
|
|
|
class PlotGenerator {
|
|
constructor({ picker = undefined, namer = undefined, rng = undefined, flipGenders = undefined } = {}) {
|
|
this._namer = namer || createRandomNamer();
|
|
this._picker = picker || createRandomPicker();
|
|
this._flipGenders = flipGenders;
|
|
}
|
|
|
|
get flipGenders() { return this._flipGenders; }
|
|
set flipGenders(flip) { this._flipGenders = flip; }
|
|
|
|
generate() {
|
|
let flip = this._flipGenders;
|
|
if (flip === undefined) {
|
|
flip = this._picker([true, false], 'flip genders');
|
|
}
|
|
|
|
let rootTransform = {};
|
|
if (this._flipGenders) {
|
|
rootTransform = {
|
|
"A": "B",
|
|
"A-2": "B-2",
|
|
"A-3": "B-3",
|
|
"A-4": "B-4",
|
|
"A-5": "B-5",
|
|
"A-6": "B-6",
|
|
"A-7": "B-7",
|
|
"A-8": "B-8",
|
|
"A-9": "B-9",
|
|
"B": "A",
|
|
"B-2": "A-2",
|
|
"B-3": "A-3",
|
|
"B-4": "A-4",
|
|
"B-5": "A-5",
|
|
"B-6": "A-6",
|
|
"B-7": "A-7",
|
|
"B-8": "A-8",
|
|
"B-9": "A-9"
|
|
};
|
|
}
|
|
|
|
let preamble = this._picker(plotto.masterClauseA, 'master clause A');
|
|
let resolution = this._picker(plotto.masterClauseC, 'master clause C');
|
|
let masterPlot = this._picker(plotto.masterClauseB, 'master clause B');
|
|
|
|
let subject = [masterPlot.group, ' / ', masterPlot.subgroup, ': ', masterPlot.description].join('');
|
|
|
|
let conflict = plotto.conflicts[this._picker(masterPlot.nodes, 'main conflict')];
|
|
let cast = [];
|
|
let plot = this._expand(conflict, rootTransform, { leadIns: 1, carryOns: 1 }).replace(/\*/g, '');
|
|
|
|
plot = this._applyNames(plot, cast);
|
|
|
|
return {
|
|
subject,
|
|
group: masterPlot.group,
|
|
subgroup: masterPlot.subgroup,
|
|
description: masterPlot.description,
|
|
cast: cast,
|
|
plot: [
|
|
subject,
|
|
preamble,
|
|
plot,
|
|
resolution
|
|
].join('\n\n').trim()
|
|
};
|
|
}
|
|
|
|
_transformToRegex(o) {
|
|
let keys = Object.keys(o);
|
|
keys.sort(function (a, b) { return b.length - a.length; });
|
|
|
|
if (keys.length == 0) {
|
|
return null;
|
|
}
|
|
|
|
let pattern = '\\b(?:' + keys.map(function (s) { return '(?:' + regEscape(s) + ')'; }).join('|') + ')(?![a-z])';
|
|
return new RegExp(pattern, 'g');
|
|
}
|
|
|
|
_applyNames(text, cast) {
|
|
|
|
//reset default name lists
|
|
maleNames = require('./data/names_male.json').slice(0);
|
|
femaleNames = require('./data/names_female.json').slice(0);
|
|
|
|
//randomNamer(characterSymbol, symbolDescription, gender)
|
|
|
|
let nameCache = {};
|
|
|
|
let rg = this._transformToRegex(plotto.characters);
|
|
if (rg) {
|
|
|
|
text = text.replace(rg, (match) => {
|
|
if (!match || match.length == 0)
|
|
return '';
|
|
|
|
let t = plotto.characters[match];
|
|
if (!t) {
|
|
console.log('Could not replace match in template: ' + match);
|
|
return match;
|
|
}
|
|
|
|
let ret = nameCache[match];
|
|
if (!ret) {
|
|
|
|
ret = this._namer(match, t, genderMap[match]);
|
|
nameCache[match] = ret;
|
|
|
|
cast.push({
|
|
symbol: match,
|
|
name: ret,
|
|
description: t
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
}
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
_expand(item, transform, ctx, start, end) {
|
|
|
|
let ret = [];
|
|
|
|
if (!item)
|
|
return 'NULL';
|
|
|
|
if (ctx.leadIns > 0 && item.leadIns) {
|
|
ctx.leadIns -= 1;
|
|
ret.push(this._expand(item.leadIns, null, ctx));
|
|
}
|
|
|
|
let carryon = null;
|
|
if (ctx.carryOns > 0 && item.carryOns) {
|
|
ctx.carryOns -= 1;
|
|
carryon = this._expand(item.carryOns, transform, ctx);
|
|
}
|
|
|
|
if (typeof (item) == "string") {
|
|
ret.push(this._expand(plotto.conflicts[item], null, ctx));
|
|
} else if (Array.isArray(item)) {
|
|
ret.push(this._expand(this._picker(item, 'plot option'), null, ctx));
|
|
} else if (item.conflictid) {
|
|
|
|
if (typeof (item.description) == "string") {
|
|
ret.push(item.description);
|
|
} else {
|
|
for (let subdesc of item.description) {
|
|
|
|
if (typeof (subdesc) == "string") {
|
|
ret.push(subdesc);
|
|
} else {
|
|
ret.push(this._expand(subdesc, null, ctx));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
} else if (item.v) {
|
|
|
|
if (item.start || item.end) {
|
|
ret.push(this._expand(plotto.conflicts[item.v], item.tfm, ctx, item.start, item.end));
|
|
} else if (item.op == '+') {
|
|
for (let sub of item.v) {
|
|
ret.push(this._expand(sub, item.tfm, ctx));
|
|
}
|
|
} else {
|
|
ret.push(this._expand(item.v, item.tfm, ctx));
|
|
}
|
|
}
|
|
|
|
if (carryon) {
|
|
ret.push(carryon);
|
|
}
|
|
|
|
let str = ret.join('\n\n').trim();
|
|
|
|
if (transform) {
|
|
let rg = this._transformToRegex(transform);
|
|
if (rg) {
|
|
|
|
let cmp = {
|
|
before: str,
|
|
transform: transform
|
|
};
|
|
str = str.replace(rg, function (match) {
|
|
if (!match || match.length == 0)
|
|
return '';
|
|
let t = transform[match];
|
|
if (!t) {
|
|
console.log('Could not replace match in template: ' + match);
|
|
return match;
|
|
}
|
|
|
|
return t;
|
|
});
|
|
|
|
cmp.after = str;
|
|
//console.log(cmp);
|
|
}
|
|
}
|
|
|
|
return str;
|
|
}
|
|
}
|
|
|
|
module.exports = PlotGenerator;
|
|
module.exports.defaultSeed = defaultSeed; |