0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-31 08:42:40 +00:00

Merge branch 'master' into experimentalNotificationDB

This commit is contained in:
G.Ambatte
2023-08-28 07:51:31 +12:00
committed by GitHub
19 changed files with 1251 additions and 791 deletions

View File

@@ -80,6 +80,53 @@ pre {
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Thursday 17/08/2023 - v3.9.2
{{taskList
##### Calculuschild
* [x] Fix links to certain old Google Drive files
Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
##### G-Ambatte
* [x] Menus now open on click, and internally consistent
Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
* [x] Add smarter footer snippet
Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
* [x] Add sanitization in Style editor
Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
* [x] Rework class table snippets to remove unnecessary randomness
Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
* [x] Add default save location selection to Account Page
Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
##### 5e-Cleric
* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
##### Gazook89
* [x] Adjustments to improve mobile viewing
}}
### Wednesday 28/06/2023 - v3.9.1 ### Wednesday 28/06/2023 - v3.9.1
{{taskList {{taskList

View File

@@ -147,11 +147,11 @@ const BrewRenderer = createClass({
}, },
renderPage : function(pageText, index){ renderPage : function(pageText, index){
const cleanPageText = this.sanitizeScriptTags(pageText); let cleanPageText = this.sanitizeScriptTags(pageText);
if(this.props.renderer == 'legacy') if(this.props.renderer == 'legacy')
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />; return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
else { else {
pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) cleanPageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
return ( return (
<div className='page' id={`p${index + 1}`} key={index} > <div className='page' id={`p${index + 1}`} key={index} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} /> <div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />

View File

@@ -16,6 +16,8 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx'); const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
let SAVEKEY = '';
const AccountPage = createClass({ const AccountPage = createClass({
displayName : 'AccountPage', displayName : 'AccountPage',
getDefaultProps : function() { getDefaultProps : function() {
@@ -29,6 +31,27 @@ const AccountPage = createClass({
uiItems : this.props.uiItems uiItems : this.props.uiItems
}; };
}, },
componentDidMount : function(){
if(!this.state.saveLocation && this.props.uiItems.username) {
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
this.makeActive(saveLocation);
}
},
makeActive : function(newSelection){
if(this.state.saveLocation == newSelection) return;
window.localStorage.setItem(SAVEKEY, newSelection);
this.setState({
saveLocation : newSelection
});
},
renderButton : function(name, key, shouldRender=true){
if(!shouldRender) return;
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
},
renderNavItems : function() { renderNavItems : function() {
return <Navbar> return <Navbar>
@@ -61,6 +84,11 @@ const AccountPage = createClass({
</p> </p>
} }
</div> </div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
</div>
</>; </>;
}, },

View File

@@ -7,6 +7,7 @@ const moment = require('moment');
const request = require('../../../../utils/request-middleware.js'); const request = require('../../../../utils/request-middleware.js');
const googleDriveIcon = require('../../../../googleDrive.svg'); const googleDriveIcon = require('../../../../googleDrive.svg');
const homebreweryIcon = require('../../../../thumbnail.png');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const BrewItem = createClass({ const BrewItem = createClass({
@@ -90,11 +91,17 @@ const BrewItem = createClass({
</a>; </a>;
}, },
renderGoogleDriveIcon : function(){ renderStorageIcon : function(){
if(!this.props.brew.googleId) return; if(this.props.brew.googleId) {
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
<a href={this.props.brew.webViewLink} target='_blank'>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
</a>
</span>;
}
return <span> return <span title='Homebrewery Storage'>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' /> <img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
</span>; </span>;
}, },
@@ -144,7 +151,7 @@ const BrewItem = createClass({
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}> Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()} <i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
</span> </span>
{this.renderGoogleDriveIcon()} {this.renderStorageIcon()}
</div> </div>
<div className='links'> <div className='links'>

View File

@@ -98,4 +98,11 @@
padding : 0px; padding : 0px;
margin : -5px; margin : -5px;
} }
.homebreweryIcon {
mix-blend-mode : darken;
height : 24px;
position : relative;
top : 5px;
left : -5px;
}
} }

View File

@@ -16,6 +16,23 @@
margin : 5px 0px; margin : 5px 0px;
border : 2px solid black; border : 2px solid black;
border-radius : 5px; border-radius : 5px;
button {
background-color : transparent;
border : 1px solid black;
border-radius : 5px;
width : 125px;
color : black;
margin-right : 5px;
&.active {
background-color: #0007;
color: white;
&:before {
content: '\f00c';
font-family: 'FONT AWESOME 5 FREE';
margin-right: 5px;
}
}
}
} }
h1, h2, h3, h4 { h1, h2, h3, h4 {
width : 100%; width : 100%;

View File

@@ -20,9 +20,10 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const BREWKEY = 'homebrewery-new'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta'; const METAKEY = 'homebrewery-new-meta';
let SAVEKEY;
const NewPage = createClass({ const NewPage = createClass({
@@ -62,12 +63,16 @@ const NewPage = createClass({
brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme; brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang; brew.lang = metaStorage?.lang ?? brew.lang;
this.setState({
brew : brew
});
} }
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
this.setState({
brew : brew,
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
});
localStorage.setItem(BREWKEY, brew.text); localStorage.setItem(BREWKEY, brew.text);
if(brew.style) if(brew.style)
localStorage.setItem(STYLEKEY, brew.style); localStorage.setItem(STYLEKEY, brew.style);

View File

@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" /> <link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href=${`/${name}/bundle.css`} rel='stylesheet' /> <link href=${`/${name}/bundle.css`} rel='stylesheet' />

View File

@@ -13,7 +13,7 @@ npm install
npm audit fix npm audit fix
npm run postinstall npm run postinstall
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/ cp install/freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
chmod +x /usr/local/etc/rc.d/homebrewery chmod +x /usr/local/etc/rc.d/homebrewery
sysrc homebrewery_enable=YES sysrc homebrewery_enable=YES

1509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.9.1", "version": "3.9.2",
"engines": { "engines": {
"node": ">=18.16.x" "node": ">=18.16.x"
}, },
@@ -78,9 +78,9 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.22.8", "@babel/core": "^7.22.10",
"@babel/plugin-transform-runtime": "^7.22.7", "@babel/plugin-transform-runtime": "^7.22.10",
"@babel/preset-env": "^7.22.7", "@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5", "@babel/preset-react": "^7.22.5",
"@googleapis/drive": "^5.1.0", "@googleapis/drive": "^5.1.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
@@ -99,30 +99,30 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "5.1.1", "marked": "5.1.1",
"marked-extended-tables": "^1.0.6", "marked-extended-tables": "^1.0.6",
"marked-gfm-heading-id": "^3.0.4", "marked-gfm-heading-id": "^3.0.6",
"marked-smartypants-lite": "^1.0.0", "marked-smartypants-lite": "^1.0.0",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4", "moment": "^2.29.4",
"mongoose": "^7.3.2", "mongoose": "^7.4.3",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"npm": "^9.8.0", "npm": "^9.8.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.14.1", "react-router-dom": "6.15.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.44.0", "eslint": "^8.47.0",
"eslint-plugin-jest": "^27.2.2", "eslint-plugin-jest": "^27.2.3",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.33.2",
"jest": "^29.6.1", "jest": "^29.6.2",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^15.10.1", "stylelint": "^15.10.3",
"stylelint-config-recess-order": "^4.3.0", "stylelint-config-recess-order": "^4.3.0",
"stylelint-config-recommended": "^13.0.0", "stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3", "stylelint-stylistic": "^0.4.3",

View File

@@ -257,6 +257,7 @@ app.get('/user/:username', async (req, res, next)=>{
brew.pageCount = googleBrews[match].pageCount; brew.pageCount = googleBrews[match].pageCount;
brew.renderer = googleBrews[match].renderer; brew.renderer = googleBrews[match].renderer;
brew.version = googleBrews[match].version; brew.version = googleBrews[match].version;
brew.webViewLink = googleBrews[match].webViewLink;
googleBrews.splice(match, 1); googleBrews.splice(match, 1);
} }
} }

View File

@@ -106,7 +106,7 @@ const GoogleActions = {
const obj = await drive.files.list({ const obj = await drive.files.list({
pageSize : 1000, pageSize : 1000,
pageToken : NextPageToken || '', pageToken : NextPageToken || '',
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)', fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false' q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
}) })
.catch((err)=>{ .catch((err)=>{
@@ -139,7 +139,8 @@ const GoogleActions = {
published : file.properties.published ? file.properties.published == 'true' : false, published : file.properties.published ? file.properties.published == 'true' : false,
systems : [], systems : [],
lang : file.properties.lang, lang : file.properties.lang,
thumbnail : file.properties.thumbnail thumbnail : file.properties.thumbnail,
webViewLink : file.webViewLink
}; };
}); });
return brews; return brews;

View File

@@ -9,6 +9,9 @@
} }
.codeEditor{ .codeEditor{
@media screen and (pointer : coarse) {
font-size : 16px;
}
.CodeMirror-foldmarker { .CodeMirror-foldmarker {
font-family: inherit; font-family: inherit;
text-shadow: none; text-shadow: none;

View File

@@ -61,7 +61,8 @@ const SplitPane = createClass({
return result; return result;
}, },
handleUp : function(){ handleUp : function(e){
e.preventDefault();
if(this.state.isDragging){ if(this.state.isDragging){
this.props.onDragFinish(this.state.currentDividerPos); this.props.onDragFinish(this.state.currentDividerPos);
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos); window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
@@ -78,6 +79,7 @@ const SplitPane = createClass({
handleMove : function(e){ handleMove : function(e){
if(!this.state.isDragging) return; if(!this.state.isDragging) return;
e.preventDefault();
const newSize = this.limitPosition(e.pageX); const newSize = this.limitPosition(e.pageX);
this.setState({ this.setState({
currentDividerPos : newSize, currentDividerPos : newSize,
@@ -122,7 +124,7 @@ const SplitPane = createClass({
renderDivider : function(){ renderDivider : function(){
return <> return <>
{this.renderMoveArrows()} {this.renderMoveArrows()}
<div className='divider' onMouseDown={this.handleDown} > <div className='divider' onPointerDown={this.handleDown} >
<div className='dots'> <div className='dots'>
<i className='fas fa-circle' /> <i className='fas fa-circle' />
<i className='fas fa-circle' /> <i className='fas fa-circle' />
@@ -133,7 +135,7 @@ const SplitPane = createClass({
}, },
render : function(){ render : function(){
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}> return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
<Pane <Pane
ref='pane1' ref='pane1'
width={this.state.currentDividerPos} width={this.state.currentDividerPos}

View File

@@ -11,6 +11,7 @@
flex : 1; flex : 1;
} }
.divider{ .divider{
touch-action : none;
display : table; display : table;
height : 100%; height : 100%;
width : 15px; width : 15px;

View File

@@ -220,34 +220,51 @@ module.exports = [
view : 'text', view : 'text',
snippets : [ snippets : [
{ {
name : 'Class Table', name : 'Class Tables',
icon : 'fas fa-table', icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,frame,decoration,wide'), gen : ClassTableGen.full('classTable,frame,decoration,wide'),
}, subsnippets : [
{ {
name : 'Class Table (unframed)', name : 'Martial Class Table',
icon : 'fas fa-border-none', icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,wide'), gen : ClassTableGen.non('classTable,frame,decoration'),
}, },
{ {
name : '1/2 Class Table', name : 'Martial Class Table (unframed)',
icon : 'fas fa-list-alt', icon : 'fas fa-border-none',
gen : ClassTableGen.half('classTable,decoration,frame'), gen : ClassTableGen.non('classTable'),
}, },
{ {
name : '1/2 Class Table (unframed)', name : 'Full Caster Class Table',
icon : 'fas fa-border-none', icon : 'fas fa-table',
gen : ClassTableGen.half('classTable'), gen : ClassTableGen.full('classTable,frame,decoration,wide'),
}, },
{ {
name : '1/3 Class Table', name : 'Full Caster Class Table (unframed)',
icon : 'fas fa-border-all', icon : 'fas fa-border-none',
gen : ClassTableGen.third('classTable,frame'), gen : ClassTableGen.full('classTable,wide'),
}, },
{ {
name : '1/3 Class Table (unframed)', name : 'Half Caster Class Table',
icon : 'fas fa-border-none', icon : 'fas fa-list-alt',
gen : ClassTableGen.third('classTable'), gen : ClassTableGen.half('classTable,frame,decoration,wide'),
},
{
name : 'Half Caster Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.half('classTable,wide'),
},
{
name : 'Third Caster Spell Table',
icon : 'fas fa-border-all',
gen : ClassTableGen.third('classTable,frame,decoration'),
},
{
name : 'Third Caster Spell Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.third('classTable'),
}
]
}, },
{ {
name : 'Rune Table', name : 'Rune Table',

View File

@@ -1,132 +1,138 @@
const _ = require('lodash'); const _ = require('lodash');
const dedent = require('dedent-tabs').default;
const features = [ const features = [
'Astrological Botany', 'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
'Biochemical Sorcery', 'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
'Civil Divination', 'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
'Consecrated Augury', 'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
'Demonic Anthropology', 'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
'Divinatory Mineralogy', 'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
'Exo Interfacer', 'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
'Genetic Banishing', 'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
'Gunpowder Torturer', 'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
'Gunslinger Corruptor', ].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
'Hermetic Geography',
'Immunological Cultist', const classnames = [
'Malefic Chemist', 'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
'Mathematical Pharmacy', 'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
'Nuclear Biochemistry',
'Orbital Gravedigger',
'Pharmaceutical Outlaw',
'Phased Linguist',
'Plasma Gunslinger',
'Police Necromancer',
'Ritual Astronomy',
'Sixgun Poisoner',
'Seismological Alchemy',
'Spiritual Illusionism',
'Statistical Occultism',
'Spell Analyst',
'Torque Interfacer'
]; ];
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
const levels = ['1st', '2nd', '3rd', '4th', '5th',
'6th', '7th', '8th', '9th', '10th',
'11th', '12th', '13th', '14th', '15th',
'16th', '17th', '18th', '19th', '20th'];
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
const drawSlots = function(Slots, rows, padding){
let slots = Number(Slots);
return _.times(rows, function(i){
const max = maxes[i];
if(slots < 1) return _.pad('—', padding);
const res = _.min([max, slots]);
slots -= res;
return _.pad(res.toString(), padding);
}).join(' | ');
};
module.exports = { module.exports = {
full : function(classes){ non : function(snippetClasses){
const classname = _.sample(classnames); return dedent`
{{${snippetClasses}
##### The ${_.sample(classnames)}
let cantrips = 3; | Level | Proficiency Bonus | Features | ${_.sample(features)} |
let spells = 1; |:-----:|:-----------------:|:---------|:---------------------:|
let slots = 2; | 1st | +2 | ${_.sample(features)} | 2 |
return `{{${classes}\n##### The ${classname}\n` + | 2nd | +2 | ${_.sample(features)} | 2 |
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+ | 3rd | +2 | ${_.sample(features)} | 3 |
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+ | 4th | +2 | ${_.sample(features)} | 3 |
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${ | 5th | +3 | ${_.sample(features)} | 3 |
_.map(levels, function(levelName, level){ | 6th | +3 | ${_.sample(features)} | 4 |
const res = [ | 7th | +3 | ${_.sample(features)} | 4 |
_.pad(levelName, 5), | 8th | +3 | ${_.sample(features)} | 4 |
_.pad(`+${profBonus[level]}`, 2), | 9th | +4 | ${_.sample(features)} | 4 |
_.padEnd(_.sample(features), 21), | 10th | +4 | ${_.sample(features)} | 4 |
_.pad(cantrips.toString(), 8), | 11th | +4 | ${_.sample(features)} | 4 |
_.pad(spells.toString(), 6), | 12th | +4 | ${_.sample(features)} | 5 |
drawSlots(slots, 9, 2), | 13th | +5 | ${_.sample(features)} | 5 |
].join(' | '); | 14th | +5 | ${_.sample(features)} | 5 |
| 15th | +5 | ${_.sample(features)} | 5 |
cantrips += _.random(0, 1); | 16th | +5 | ${_.sample(features)} | 5 |
spells += _.random(0, 1); | 17th | +6 | ${_.sample(features)} | 6 |
slots += _.random(0, 2); | 18th | +6 | ${_.sample(features)} | 6 |
| 19th | +6 | ${_.sample(features)} | 6 |
return `| ${res} |`; | 20th | +6 | ${_.sample(features)} | unlimited |
}).join('\n')}\n}}\n\n`; }}\n\n`;
}, },
half : function(classes){ full : function(snippetClasses){
const classname = _.sample(classnames); return dedent`
{{${snippetClasses}
let featureScore = 1; ##### The ${_.sample(classnames)}
return `{{${classes}\n##### The ${classname}\n` + | Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` + | ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
`|:-----:|:-----------------:|:---------|:---------------------:|\n${ |:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
_.map(levels, function(levelName, level){ | 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
const res = [ | 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
_.pad(levelName, 5), | 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
_.pad(`+${profBonus[level]}`, 2), | 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
_.padEnd(_.sample(features), 23), | 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
_.pad(`+${featureScore}`, 21), | 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
].join(' | '); | 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
featureScore += _.random(0, 1); | 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
return `| ${res} |`; | 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
}).join('\n')}\n}}\n\n`; | 12th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
| 13th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
| 14th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
| 15th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
| 16th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
| 17th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | 1 |
| 18th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 1 | 1 | 1 | 1 | 1 |
| 19th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 1 |
| 20th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 |
}}\n\n`;
}, },
third : function(classes){ half : function(snippetClasses){
const classname = _.sample(classnames); return dedent`
{{${snippetClasses}
##### The ${_.sample(classnames)}
| Level | Proficiency | Features | Spells |--- Spell Slots Per Spell Level ---|||||
| ^| Bonus ^| ^| Known ^| 1st | 2nd | 3rd | 4th | 5th |
|:-----:|:-----------:|:-------------|:------:|:-----:|:-----:|:-----:|:-----:|:-----:|
| 1st | +2 | ${_.sample(features)} | — | — | — | — | — | — |
| 2nd | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — |
| 3rd | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
| 4th | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
| 5th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
| 6th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
| 7th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
| 8th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
| 9th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
| 10th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
| 11th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
| 12th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
| 13th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
| 14th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
| 15th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
| 16th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
| 17th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
| 18th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
| 19th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
| 20th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
}}\n\n`;
},
let cantrips = 3; third : function(snippetClasses){
let spells = 1; return dedent`
let slots = 2; {{${snippetClasses}
return `{{${classes}\n##### ${classname} Spellcasting\n` + ##### ${_.sample(classnames)} Spellcasting
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` + | Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` + | ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${ |:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
_.map(levels, function(levelName, level){ | 3rd | 2 | 3 | 2 | — | — | — |
const res = [ | 4th | 2 | 4 | 3 | — | — | — |
_.pad(levelName, 6), | 5th | 2 | 4 | 3 | — | — | — |
_.pad(cantrips.toString(), 8), | 6th | 2 | 4 | 3 | — | — | — |
_.pad(spells.toString(), 7), | 7th | 2 | 5 | 4 | 2 | — | — |
drawSlots(slots, 4, 7), | 8th | 2 | 6 | 4 | 2 | — | — |
].join(' | '); | 9th | 2 | 6 | 4 | 2 | — | — |
| 10th | 3 | 7 | 4 | 3 | — | — |
cantrips += _.random(0, 1); | 11th | 3 | 8 | 4 | 3 | — | — |
spells += _.random(0, 1); | 12th | 3 | 8 | 4 | 3 | — | — |
slots += _.random(0, 1); | 13th | 3 | 9 | 4 | 3 | 2 | — |
| 14th | 3 | 10 | 4 | 3 | 2 | — |
return `| ${res} |`; | 15th | 3 | 10 | 4 | 3 | 2 | — |
}).join('\n')}\n}}\n\n`; | 16th | 3 | 11 | 4 | 3 | 3 | — |
| 17th | 3 | 11 | 4 | 3 | 3 | — |
| 18th | 3 | 11 | 4 | 3 | 3 | — |
| 19th | 3 | 12 | 4 | 3 | 3 | 1 |
| 20th | 3 | 13 | 4 | 3 | 3 | 1 |
}}\n\n`;
} }
}; };

View File

@@ -29,21 +29,23 @@ const getTOC = (pages)=>{
const res = []; const res = [];
_.each(pages, (page, pageNum)=>{ _.each(pages, (page, pageNum)=>{
const lines = page.split('\n'); if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
_.each(lines, (line)=>{ const lines = page.split('\n');
if(_.startsWith(line, '# ')){ _.each(lines, (line)=>{
const title = line.replace('# ', ''); if(_.startsWith(line, '# ')){
add1(title, pageNum); const title = line.replace('# ', '');
} add1(title, pageNum);
if(_.startsWith(line, '## ')){ }
const title = line.replace('## ', ''); if(_.startsWith(line, '## ')){
add2(title, pageNum); const title = line.replace('## ', '');
} add2(title, pageNum);
if(_.startsWith(line, '### ')){ }
const title = line.replace('### ', ''); if(_.startsWith(line, '### ')){
add3(title, pageNum); const title = line.replace('### ', '');
} add3(title, pageNum);
}); }
});
}
}); });
return res; return res;
}; };