diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 2cdac284c..24c48b12b 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -23,6 +23,7 @@ const BrewRenderer = createClass({ text : '', style : '', renderer : 'legacy', + theme : '5ePHB', errors : [] }; }, @@ -177,6 +178,8 @@ const BrewRenderer = createClass({ render : function(){ //render in iFrame so broken code doesn't crash the site. //Also render dummy page while iframe is mounting. + const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy'; + const themePath = this.props.theme ?? '5ePHB'; return ( @@ -200,7 +203,7 @@ const BrewRenderer = createClass({ - + {/* Apply CSS from Style tab and render pages from Markdown tab */} {this.state.isMounted && diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 75d489087..2768597da 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -1,3 +1,4 @@ +/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./metadataEditor.less'); const React = require('react'); const createClass = require('create-react-class'); @@ -29,6 +30,11 @@ const MetadataEditor = createClass({ }; }, + getThemeData : function(renderer, theme){ + + return Themes[_.upperFirst(renderer)].find((x)=>x.path == theme); + }, + handleFieldChange : function(name, e){ this.props.onChange(_.merge({}, this.props.metadata, { [name] : e.target.value @@ -45,6 +51,8 @@ const MetadataEditor = createClass({ handleRenderer : function(renderer, e){ if(e.target.checked){ this.props.metadata.renderer = renderer; + if(renderer == 'legacy') + this.props.metadata.theme = '5ePHB'; } this.props.onChange(this.props.metadata); }, @@ -56,7 +64,7 @@ const MetadataEditor = createClass({ handleTheme : function(theme){ this.props.metadata.renderer = theme.renderer; - this.props.metadata.theme = theme.name; + this.props.metadata.theme = theme.path; this.props.onChange(this.props.metadata); }, @@ -129,17 +137,19 @@ const MetadataEditor = createClass({ renderThemeDropdown : function(){ const listThemes = (renderer)=>{ return _.map(Themes[renderer], (theme)=>{ - return
this.handleTheme(theme)} title={''}> + return
this.handleTheme(theme)} title={''}> {`${theme.renderer} : ${theme.name}`}
; }); }; + const currentTheme = this.getThemeData(this.props.metadata.renderer, this.props.metadata.theme); + return
- {`${this.props.metadata.renderer} : ${this.props.metadata.theme}`} + {`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`}
{listThemes('Legacy')} {listThemes('V3')} diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index cf9dd1166..1dc0b1231 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -42,8 +42,6 @@ const Snippetbar = createClass({ componentDidMount : async function() { const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy'; const themePath = this.props.theme ?? '5ePHB'; - console.log(Themes); - console.log(Themes[`${rendererPath}_${themePath}`]); const snippets = Themes[`${rendererPath}_${themePath}`]; this.setState({ snippets : snippets diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d2f21481d..17dfd865b 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -472,7 +472,7 @@ const EditPage = createClass({ onMetaChange={this.handleMetaChange} renderer={this.state.brew.renderer} /> - +
; diff --git a/scripts/buildHomebrew.js b/scripts/buildHomebrew.js index 7ae28ddf5..17605e2a1 100644 --- a/scripts/buildHomebrew.js +++ b/scripts/buildHomebrew.js @@ -50,32 +50,36 @@ fs.emptyDirSync('./build'); let themeFiles = fs.readdirSync('./themes/Legacy'); for (dir of themeFiles) { const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString()); + themeData.path = dir; themes.Legacy.push(themeData); + const src = `./themes/Legacy/${dir}/style.less`; + ((outputDirectory)=>{ + less.render(fs.readFileSync(src).toString(), { + compress : !isDev + }, function(e, output) { + fs.outputFile(`./build/themes/Legacy/${dir}/style.css`, output.css); + }); + })(`./build/themes/Legacy/${dir}/style.css`); + } themeFiles = fs.readdirSync('./themes/V3'); for (dir of themeFiles) { const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString()); + themeData.path = dir; themes.V3.push(themeData); + const src = `./themes/V3/${dir}/style.less`; + ((outputDirectory)=>{ + less.render(fs.readFileSync(src).toString(), { + compress : !isDev + }, function(e, output) { + fs.outputFile(outputDirectory, output.css); + }); + })(`./build/themes/V3/${dir}/style.css`); } await fs.outputFile('./themes/themes.json', JSON.stringify(themes, null, 2)); - // Compile Less files TODO: MOVE INTO ABOVE SECTION TO PROGRAMATICALLY GRAB ALL LESS FILES - let src = './themes/Legacy/5ePHB/style.less'; - - less.render(fs.readFileSync(src).toString(), { - compress : !isDev - }, function(e, output) { - fs.outputFile('./build/themes/Legacy/5ePHB/style.css', output.css); - }); - src = './themes/V3/5ePHB/style.less'; - less.render(fs.readFileSync(src).toString(), { - compress : !isDev - }, function(e, output) { - fs.outputFile('./build/themes/V3/5ePHB/style.css', output.css); - }); - // await less.render(lessCode, { // compress : !dev, // sourceMap : (dev ? { @@ -86,6 +90,7 @@ fs.emptyDirSync('./build'); // Move assets await fs.copy('./themes/fonts', './build/fonts'); + await fs.copy('./themes/assets', './build/assets'); //v==----------------------------- BUNDLE PACKAGES --------------------------------==v// diff --git a/themes/V3/5eDMG/settings.json b/themes/V3/5eDMG/settings.json new file mode 100644 index 000000000..99ea7f54a --- /dev/null +++ b/themes/V3/5eDMG/settings.json @@ -0,0 +1,5 @@ +{ + "name" : "5e DMG", + "renderer" : "V3", + "baseTheme" : "5ePHB" +} diff --git a/themes/V3/5eDMG/snippets.js b/themes/V3/5eDMG/snippets.js new file mode 100644 index 000000000..727e17359 --- /dev/null +++ b/themes/V3/5eDMG/snippets.js @@ -0,0 +1,404 @@ +/* eslint-disable max-lines */ + +const MagicGen = require('./snippets/magic.gen.js'); +const ClassTableGen = require('./snippets/classtable.gen.js'); +const MonsterBlockGen = require('./snippets/monsterblock.gen.js'); +const ClassFeatureGen = require('./snippets/classfeature.gen.js'); +const CoverPageGen = require('./snippets/coverpage.gen.js'); +const TableOfContentsGen = require('./snippets/tableOfContents.gen.js'); +const WatercolorGen = require('./snippets/watercolor.gen.js'); +const dedent = require('dedent-tabs').default; + + + +module.exports = [ + + { + groupName : 'Text Editor', + icon : 'fas fa-pencil-alt', + view : 'text', + snippets : [ + { + name : 'Column Break', + icon : 'fas fa-columns', + gen : '\n\\column\n' + }, + { + name : 'New Page', + icon : 'fas fa-file-alt', + gen : '\n\\page\n' + }, + { + name : 'Vertical Spacing', + icon : 'fas fa-arrows-alt-v', + gen : '\n::::\n' + }, + { + name : 'Horizontal Spacing', + icon : 'fas fa-arrows-alt-h', + gen : ' {{width:100px}} ' + }, + { + name : 'Wide Block', + icon : 'fas fa-window-maximize', + gen : dedent`\n + {{wide + Everything in here will be extra wide. Tables, text, everything! + Beware though, CSS columns can behave a bit weird sometimes. You may + have to manually place column breaks with \`\column\` to make the + surrounding text flow with this wide block the way you want. + }} + \n` + }, + { + name : 'QR Code', + icon : 'fas fa-qrcode', + gen : (brew)=>{ + return `![]` + + `(https://api.qrserver.com/v1/create-qr-code/?data=` + + `https://homebrewery.naturalcrit.com${brew.shareId ? `/share/${brew.shareId}` : ''}` + + `&size=100x100) {width:100px;mix-blend-mode:multiply}`; + } + }, + { + name : 'Page Number', + icon : 'fas fa-bookmark', + gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n' + }, + { + name : 'Auto-incrementing Page Number', + icon : 'fas fa-sort-numeric-down', + gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n' + }, + { + name : 'Link to page', + icon : 'fas fa-link', + gen : '[Click here](#p3) to go to page 3\n' + }, + { + name : 'Table of Contents', + icon : 'fas fa-book', + gen : TableOfContentsGen + }, + { + name : 'Add Comment', + icon : 'fas fa-code', + gen : '' + }, + ] + }, + { + groupName : 'Style Editor', + icon : 'fas fa-pencil-alt', + view : 'style', + snippets : [ + { + name : 'Remove Drop Cap', + icon : 'fas fa-remove-format', + gen : dedent`/* Removes Drop Caps */ + .page h1+p:first-letter { + all: unset; + }\n\n` + }, + { + name : 'Tweak Drop Cap', + icon : 'fas fa-sliders-h', + gen : dedent`/* Drop Cap settings */ + .page h1 + p::first-letter { + font-family: SolberaImitationRemake; + font-size: 3.5cm; + background-image: linear-gradient(-45deg, #322814, #998250, #322814); + line-height: 1em; + }\n\n` + }, + { + name : 'Add Comment', + icon : 'fas fa-code', + gen : '/* This is a comment that will not be rendered into your brew. */' + }, + ] + }, + + /*********************** IMAGES *******************/ + { + groupName : 'Images', + icon : 'fas fa-images', + view : 'text', + snippets : [ + { + name : 'Image', + icon : 'fas fa-image', + gen : dedent` + ![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px,mix-blend-mode:multiply} + + {{artist,position:relative,top:-230px,left:10px,margin-bottom:-30px + ##### Cat Warrior + [Kyoung Hwan Kim](https://www.artstation.com/tahra) + }}` + }, + { + name : 'Background Image', + icon : 'fas fa-tree', + gen : dedent` + ![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,top:50px,right:30px,width:280px} + + {{artist,top:80px,right:30px + ##### Homebrew Mug + [naturalcrit](https://homebrew.naturalcrit.com) + }}` + }, + { + name : 'Watercolor Splatter', + icon : 'fas fa-fill-drip', + gen : WatercolorGen, + }, + { + name : 'Watermark', + icon : 'fas fa-id-card', + gen : dedent` + {{watermark Homebrewery}}\n` + }, + ] + }, + + + /************************* PHB ********************/ + + { + groupName : 'PHB', + icon : 'fas fa-book', + view : 'text', + snippets : [ + { + name : 'Spell', + icon : 'fas fa-magic', + gen : MagicGen.spell, + }, + { + name : 'Spell List', + icon : 'fas fa-scroll', + gen : MagicGen.spellList, + }, + { + name : 'Class Feature', + icon : 'fas fa-mask', + gen : ClassFeatureGen, + }, + { + name : 'Note', + icon : 'fas fa-sticky-note', + gen : function(){ + return dedent` + {{note + ##### Time to Drop Knowledge + Use notes to point out some interesting information. + + **Tables and lists** both work within a note. + }} + \n`; + }, + }, + { + name : 'Descriptive Text Box', + icon : 'fas fa-comment-alt', + gen : function(){ + return dedent` + {{descriptive + ##### Time to Drop Knowledge + Use descriptive boxes to highlight text that should be read aloud. + + **Tables and lists** both work within a descriptive box. + }} + \n`; + }, + }, + { + name : 'Monster Stat Block (unframed)', + icon : 'fas fa-paw', + gen : MonsterBlockGen.monster('monster', 2), + }, + { + name : 'Monster Stat Block', + icon : 'fas fa-spider', + gen : MonsterBlockGen.monster('monster,frame', 2), + }, + { + name : 'Wide Monster Stat Block', + icon : 'fas fa-dragon', + gen : MonsterBlockGen.monster('monster,frame,wide', 4), + }, + { + name : 'Cover Page', + icon : 'fas fa-file-word', + gen : CoverPageGen, + }, + { + name : 'Magic Item', + icon : 'fas fa-hat-wizard', + gen : MagicGen.item, + }, + { + name : 'Artist Credit', + icon : 'fas fa-signature', + gen : function(){ + return dedent` + {{artist,top:90px,right:30px + ##### Starry Night + [Van Gogh](https://www.vangoghmuseum.nl/en) + }} + \n`; + }, + }, + ] + }, + + + + /********************* TABLES *********************/ + + { + groupName : 'Tables', + icon : 'fas fa-table', + view : 'text', + snippets : [ + { + name : 'Table', + icon : 'fas fa-th-list', + gen : function(){ + return dedent` + ##### Character Advancement + | Experience Points | Level | Proficiency Bonus | + |:------------------|:-----:|:-----------------:| + | 0 | 1 | +2 | + | 300 | 2 | +2 | + | 900 | 3 | +2 | + | 2,700 | 4 | +2 | + | 6,500 | 5 | +3 | + | 14,000 | 6 | +3 | + \n`; + } + }, + { + name : 'Wide Table', + icon : 'fas fa-list', + gen : function(){ + return dedent` + {{wide + ##### Weapons + | Name | Cost | Damage | Weight | Properties | + |:------------------------|:-----:|:----------------|--------:|:-----------| + | *Simple Melee Weapons* | | | | | + |   Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light | + |   Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse | + |   Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown | + | *Simple Ranged Weapons* | | | | | + |   Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse | + |   Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition | + |   Sling | 1 sp | 1d4 bludgeoning | — | Ammunition | + }} + \n`; + } + }, + { + name : 'Split Table', + icon : 'fas fa-th-large', + gen : function(){ + return dedent` + ##### Typical Difficulty Classes + {{column-count:2 + | Task Difficulty | DC | + |:----------------|:--:| + | Very easy | 5 | + | Easy | 10 | + | Medium | 15 | + + | Task Difficulty | DC | + |:------------------|:--:| + | Hard | 20 | + | Very hard | 25 | + | Nearly impossible | 30 | + }} + \n`; + } + }, + { + name : 'Class Table', + icon : 'fas fa-table', + gen : ClassTableGen.full('classTable,frame,decoration,wide'), + }, + { + name : 'Class Table (unframed)', + icon : 'fas fa-border-none', + gen : ClassTableGen.full('classTable,wide'), + }, + { + name : '1/2 Class Table', + icon : 'fas fa-list-alt', + gen : ClassTableGen.half('classTable,decoration,frame'), + }, + { + name : '1/2 Class Table (unframed)', + icon : 'fas fa-border-none', + gen : ClassTableGen.half('classTable'), + }, + { + name : '1/3 Class Table', + icon : 'fas fa-border-all', + gen : ClassTableGen.third('classTable,frame'), + }, + { + name : '1/3 Class Table (unframed)', + icon : 'fas fa-border-none', + gen : ClassTableGen.third('classTable'), + } + ] + }, + + + + + /**************** PAGE *************/ + + { + groupName : 'Print', + icon : 'fas fa-print', + view : 'style', + snippets : [ + { + name : 'A4 Page Size', + icon : 'far fa-file', + gen : dedent`/* A4 Page Size */ + .page{ + width : 210mm; + height : 296.8mm; + }\n\n` + }, + { + name : 'Square Page Size', + icon : 'far fa-file', + gen : dedent`/* Square Page Size */ + .page { + width : 125mm; + height : 125mm; + padding : 12.5mm; + columns : unset; + }\n\n` + }, + { + name : 'Ink Friendly', + icon : 'fas fa-tint', + gen : dedent` + /* Ink Friendly */ + *:is(.page,.monster,.note,.descriptive) { + background : white !important; + filter : drop-shadow(0px 0px 3px #888) !important; + } + + .page img { + visibility : hidden; + }\n\n` + }, + ] + }, + +]; diff --git a/themes/V3/5eDMG/snippets/classfeature.gen.js b/themes/V3/5eDMG/snippets/classfeature.gen.js new file mode 100644 index 000000000..7e2e7e858 --- /dev/null +++ b/themes/V3/5eDMG/snippets/classfeature.gen.js @@ -0,0 +1,42 @@ +const _ = require('lodash'); +const dedent = require('dedent-tabs').default; + +module.exports = function(classname){ + + classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher', + 'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']); + + classname = classname.toLowerCase(); + + const hitDie = _.sample([4, 6, 8, 10, 12]); + + const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence']; + const skillList = ['Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival']; + + + return dedent` + ## Class Features + As a ${classname}, you gain the following class features + #### Hit Points + + **Hit Dice:** :: 1d${hitDie} per ${classname} level + **Hit Points at 1st Level:** :: ${hitDie} + your Constitution modifier + **Hit Points at Higher Levels:** :: 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st + + #### Proficiencies + + **Armor:** :: ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'} + **Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'} + **Tools:** :: ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'} + + **Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')} + **Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')} + + #### Equipment + You start with the following equipment, in addition to the equipment granted by your background: + - *(a)* a martial weapon and a shield or *(b)* two martial weapons + - *(a)* five javelins or *(b)* any simple melee weapon + - ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])} + + `; +}; diff --git a/themes/V3/5eDMG/snippets/classtable.gen.js b/themes/V3/5eDMG/snippets/classtable.gen.js new file mode 100644 index 000000000..c1f6254f9 --- /dev/null +++ b/themes/V3/5eDMG/snippets/classtable.gen.js @@ -0,0 +1,132 @@ +const _ = require('lodash'); + +const features = [ + 'Astrological Botany', + 'Biochemical Sorcery', + 'Civil Divination', + 'Consecrated Augury', + 'Demonic Anthropology', + 'Divinatory Mineralogy', + 'Exo Interfacer', + 'Genetic Banishing', + 'Gunpowder Torturer', + 'Gunslinger Corruptor', + 'Hermetic Geography', + 'Immunological Cultist', + 'Malefic Chemist', + 'Mathematical Pharmacy', + '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 = { + full : function(classes){ + const classname = _.sample(classnames); + + + let cantrips = 3; + let spells = 1; + let slots = 2; + return `{{${classes}\n##### The ${classname}\n` + + `| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+ + `| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+ + `|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${ + _.map(levels, function(levelName, level){ + const res = [ + _.pad(levelName, 5), + _.pad(`+${profBonus[level]}`, 2), + _.padEnd(_.sample(features), 21), + _.pad(cantrips.toString(), 8), + _.pad(spells.toString(), 6), + drawSlots(slots, 9, 2), + ].join(' | '); + + cantrips += _.random(0, 1); + spells += _.random(0, 1); + slots += _.random(0, 2); + + return `| ${res} |`; + }).join('\n')}\n}}\n\n`; + }, + + half : function(classes){ + const classname = _.sample(classnames); + + let featureScore = 1; + return `{{${classes}\n##### The ${classname}\n` + + `| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` + + `|:-----:|:-----------------:|:---------|:---------------------:|\n${ + _.map(levels, function(levelName, level){ + const res = [ + _.pad(levelName, 5), + _.pad(`+${profBonus[level]}`, 2), + _.padEnd(_.sample(features), 23), + _.pad(`+${featureScore}`, 21), + ].join(' | '); + + featureScore += _.random(0, 1); + + return `| ${res} |`; + }).join('\n')}\n}}\n\n`; + }, + + third : function(classes){ + const classname = _.sample(classnames); + + let cantrips = 3; + let spells = 1; + let slots = 2; + return `{{${classes}\n##### ${classname} Spellcasting\n` + + `| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` + + `| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` + + `|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${ + _.map(levels, function(levelName, level){ + const res = [ + _.pad(levelName, 6), + _.pad(cantrips.toString(), 8), + _.pad(spells.toString(), 7), + drawSlots(slots, 4, 7), + ].join(' | '); + + cantrips += _.random(0, 1); + spells += _.random(0, 1); + slots += _.random(0, 1); + + return `| ${res} |`; + }).join('\n')}\n}}\n\n`; + } +}; diff --git a/themes/V3/5eDMG/snippets/coverpage.gen.js b/themes/V3/5eDMG/snippets/coverpage.gen.js new file mode 100644 index 000000000..0fb8ba7a4 --- /dev/null +++ b/themes/V3/5eDMG/snippets/coverpage.gen.js @@ -0,0 +1,124 @@ +const _ = require('lodash'); + +const titles = [ + 'The Burning Gallows', + 'The Ring of Nenlast', + 'Below the Blind Tavern', + 'Below the Hungering River', + 'Before Bahamut\'s Land', + 'The Cruel Grave from Within', + 'The Strength of Trade Road', + 'Through The Raven Queen\'s Worlds', + 'Within the Settlement', + 'The Crown from Within', + 'The Merchant Within the Battlefield', + 'Ioun\'s Fading Traveler', + 'The Legion Ingredient', + 'The Explorer Lure', + 'Before the Charming Badlands', + 'The Living Dead Above the Fearful Cage', + 'Vecna\'s Hidden Sage', + 'Bahamut\'s Demonspawn', + 'Across Gruumsh\'s Elemental Chaos', + 'The Blade of Orcus', + 'Beyond Revenge', + 'Brain of Insanity', + 'Breed Battle!, A New Beginning', + 'Evil Lake, A New Beginning', + 'Invasion of the Gigantic Cat, Part II', + 'Kraken War 2020', + 'The Body Whisperers', + 'The Diabolical Tales of the Ape-Women', + 'The Doctor Immortal', + 'The Doctor from Heaven', + 'The Graveyard', + 'Azure Core', + 'Core Battle', + 'Core of Heaven: The Guardian of Amazement', + 'Deadly Amazement III', + 'Dry Chaos IX', + 'Gate Thunder', + 'Guardian: Skies of the Dark Wizard', + 'Lute of Eternity', + 'Mercury\'s Planet: Brave Evolution', + 'Ruby of Atlantis: The Quake of Peace', + 'Sky of Zelda: The Thunder of Force', + 'Vyse\'s Skies', + 'White Greatness III', + 'Yellow Divinity', + 'Zidane\'s Ghost' +]; + +const subtitles = [ + 'In an ominous universe, a botanist opposes terrorism.', + 'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.', + 'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.', + 'In an evil empire of horror, two rangers battle the forces of hell.', + 'In a lost city, in an age of sorcery, a librarian quests for revenge.', + 'In a universe of illusions and danger, three time travellers and an adventurer search for justice.', + 'In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.', + 'In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.', + 'In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.', + 'In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.', + 'In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.', + 'In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.', + 'In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.', + 'In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.', + 'In a kingdom of deception, a reporter searches for fame.', + 'In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.', + 'In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.', + 'In a dark city of confusion, three swordswomen and a singer battle lawlessness.', + 'In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.', + 'In a kingdom of panic, six adventurers oppose lawlessness.', + 'In a land of dreams and hopelessness, three hackers and a cyborg search for justice.', + 'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.', + 'In a wicked universe, five seers fight lawlessness.', + 'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.', + 'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.', + 'In a cursed empire, five inventors oppose terrorism.', + 'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.', + 'In a forgotten land, a reporter and a spy try to stop the apocalypse.', + 'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.', + 'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.', + 'In a galaxy of dark magic, four fighters seek freedom.', + 'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.', + 'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.', + 'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.', + 'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.', + 'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.', + 'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.', + 'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.', + 'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.', + 'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.', + 'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.', + 'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.', + 'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.', + 'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.', + 'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.' +]; + + +module.exports = ()=>{ + return ` + +{{margin-top:225px}} + +# ${_.sample(titles)} + +{{margin-top:25px}} + +{{wide +##### ${_.sample(subtitles)} +}} + +\\page`; +}; \ No newline at end of file diff --git a/themes/V3/5eDMG/snippets/fullclass.gen.js b/themes/V3/5eDMG/snippets/fullclass.gen.js new file mode 100644 index 000000000..5ede9e501 --- /dev/null +++ b/themes/V3/5eDMG/snippets/fullclass.gen.js @@ -0,0 +1,43 @@ +const _ = require('lodash'); + +const ClassFeatureGen = require('./classfeature.gen.js'); + +const ClassTableGen = require('./classtable.gen.js'); + +module.exports = function(){ + + const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher', + 'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']); + + + const image = _.sample(_.map([ + 'http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png', + 'http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png', + 'http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png', + 'http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png', + 'http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png', + 'http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png', + 'http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png', + 'http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png', + ], function(url){ + return ``; + })); + + + return `${[ + image, + '', + '```', + '```', + '
\n\n', + `## ${classname}`, + 'Cool intro stuff will go here', + + '\\page', + ClassTableGen(classname), + ClassFeatureGen(classname), + + + + ].join('\n')}\n\n\n`; +}; \ No newline at end of file diff --git a/themes/V3/5eDMG/snippets/magic.gen.js b/themes/V3/5eDMG/snippets/magic.gen.js new file mode 100644 index 000000000..33ce8406d --- /dev/null +++ b/themes/V3/5eDMG/snippets/magic.gen.js @@ -0,0 +1,109 @@ +const _ = require('lodash'); + +const spellNames = [ + 'Astral Rite of Acne', + 'Create Acne', + 'Cursed Ramen Erruption', + 'Dark Chant of the Dentists', + 'Erruption of Immaturity', + 'Flaming Disc of Inconvenience', + 'Heal Bad Hygene', + 'Heavenly Transfiguration of the Cream Devil', + 'Hellish Cage of Mucus', + 'Irritate Peanut Butter Fairy', + 'Luminous Erruption of Tea', + 'Mystic Spell of the Poser', + 'Sorcerous Enchantment of the Chimneysweep', + 'Steak Sauce Ray', + 'Talk to Groupie', + 'Astonishing Chant of Chocolate', + 'Astounding Pasta Puddle', + 'Ball of Annoyance', + 'Cage of Yarn', + 'Control Noodles Elemental', + 'Create Nervousness', + 'Cure Baldness', + 'Cursed Ritual of Bad Hair', + 'Dispell Piles in Dentist', + 'Eliminate Florists', + 'Illusionary Transfiguration of the Babysitter', + 'Necromantic Armor of Salad Dressing', + 'Occult Transfiguration of Foot Fetish', + 'Protection from Mucus Giant', + 'Tinsel Blast', + 'Alchemical Evocation of the Goths', + 'Call Fangirl', + 'Divine Spell of Crossdressing', + 'Dominate Ramen Giant', + 'Eliminate Vindictiveness in Gym Teacher', + 'Extra-Planar Spell of Irritation', + 'Induce Whining in Babysitter', + 'Invoke Complaining', + 'Magical Enchantment of Arrogance', + 'Occult Globe of Salad Dressing', + 'Overwhelming Enchantment of the Chocolate Fairy', + 'Sorcerous Dandruff Globe', + 'Spiritual Invocation of the Costumers', + 'Ultimate Rite of the Confetti Angel', + 'Ultimate Ritual of Mouthwash', +]; +const itemNames = [ + 'Doorknob of Niceness', + 'Paper Armor of Folding', + 'Mixtape of Sadness', + 'Staff of Endless Confetti', +]; + +module.exports = { + + spellList : function(){ + const levels = ['Cantrips (0 Level)', '1st Level', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level']; + + const content = _.map(levels, (level)=>{ + const spells = _.map(_.sampleSize(spellNames, _.random(4, 10)), (spell)=>{ + return `- ${spell}`; + }).join('\n'); + return `##### ${level} \n${spells} \n`; + }).join('\n'); + + return `{{spellList,wide\n${content}\n}}`; + }, + + spell : function(){ + const level = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th']; + const spellSchools = ['abjuration', 'conjuration', 'divination', 'enchantment', 'evocation', 'illusion', 'necromancy', 'transmutation']; + + + let components = _.sampleSize(['V', 'S', 'M'], _.random(1, 3)).join(', '); + if(components.indexOf('M') !== -1){ + components += ` (${_.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1, 3)).join(', ')})`; + } + + return [ + `#### ${_.sample(spellNames)}`, + `*${_.sample(level)}-level ${_.sample(spellSchools)}*`, + '', + '**Casting Time:** :: 1 action', + `**Range:** :: ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`, + `**Components:** :: ${components}`, + `**Duration:** :: ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`, + '', + 'A flame, equivalent in brightness to a torch, springs from an object that you touch. ', + 'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ', + 'A *continual flame* can be covered or hidden but not smothered or quenched.', + '\n\n\n' + ].join('\n'); + }, + + item : function() { + return [ + `#### ${_.sample(itemNames)}`, + `*${_.sample(['Wondrous item', 'Armor', 'Weapon'])}, ${_.sample(['Common', 'Uncommon', 'Rare', 'Very Rare', 'Legendary', 'Artifact'])} (requires attunement)*`, + `:`, + `This knob is pretty nice. When attached to a door, it allows a user to`, + `open that door with the strength of the nearest animal. For example, if`, + `there is a cow nearby, the user will have the "strength of a cow" while`, + `opening this door.` + ].join('\n'); + } +}; diff --git a/themes/V3/5eDMG/snippets/monsterblock.gen.js b/themes/V3/5eDMG/snippets/monsterblock.gen.js new file mode 100644 index 000000000..dddf9b78c --- /dev/null +++ b/themes/V3/5eDMG/snippets/monsterblock.gen.js @@ -0,0 +1,184 @@ +const _ = require('lodash'); +const dedent = require('dedent-tabs').default; + +const genList = function(list, max){ + return _.sampleSize(list, _.random(0, max)).join(', ') || 'None'; +}; + +const getMonsterName = function(){ + return _.sample([ + 'All-devouring Baseball Imp', + 'All-devouring Gumdrop Wraith', + 'Chocolate Hydra', + 'Devouring Peacock', + 'Economy-sized Colossus of the Lemonade Stand', + 'Ghost Pigeon', + 'Gibbering Duck', + 'Sparklemuffin Peacock Spider', + 'Gum Elemental', + 'Illiterate Construct of the Candy Store', + 'Ineffable Chihuahua', + 'Irritating Death Hamster', + 'Irritating Gold Mouse', + 'Juggernaut Snail', + 'Juggernaut of the Sock Drawer', + 'Koala of the Cosmos', + 'Mad Koala of the West', + 'Milk Djinni of the Lemonade Stand', + 'Mind Ferret', + 'Mystic Salt Spider', + 'Necrotic Halitosis Angel', + 'Pinstriped Famine Sheep', + 'Ritalin Leech', + 'Shocker Kangaroo', + 'Stellar Tennis Juggernaut', + 'Wailing Quail of the Sun', + 'Angel Pigeon', + 'Anime Sphinx', + 'Bored Avalanche Sheep of the Wasteland', + 'Devouring Nougat Sphinx of the Sock Drawer', + 'Djinni of the Footlocker', + 'Ectoplasmic Jazz Devil', + 'Flatuent Angel', + 'Gelatinous Duck of the Dream-Lands', + 'Gelatinous Mouse', + 'Golem of the Footlocker', + 'Lich Wombat', + 'Mechanical Sloth of the Past', + 'Milkshake Succubus', + 'Puffy Bone Peacock of the East', + 'Rainbow Manatee', + 'Rune Parrot', + 'Sand Cow', + 'Sinister Vanilla Dragon', + 'Snail of the North', + 'Spider of the Sewer', + 'Stellar Sawdust Leech', + 'Storm Anteater of Hell', + 'Stupid Spirit of the Brewery', + 'Time Kangaroo', + 'Tomb Poodle', + ]); +}; + +const getType = function(){ + return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`; +}; + +const getAlignment = function(){ + return _.sample([ + 'annoying evil', + 'chaotic gossipy', + 'chaotic sloppy', + 'depressed neutral', + 'lawful bogus', + 'lawful coy', + 'manic-depressive evil', + 'narrow-minded neutral', + 'neutral annoying', + 'neutral ignorant', + 'oedpipal neutral', + 'silly neutral', + 'unoriginal neutral', + 'weird neutral', + 'wordy evil', + 'unaligned' + ]); +}; + +const getStats = function(){ + return `|${_.times(6, function(){ + const num = _.random(1, 20); + const mod = Math.ceil(num/2 - 5); + return `${num} (${mod >= 0 ? `+${mod}` : mod})`; + }).join('|')}|`; +}; + +const genAbilities = function(){ + return _.sample([ + '***Pack Tactics.*** These guys work together like peanut butter and jelly.', + '***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.', + '***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.', + '***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.', + '***Sassiness.*** When questioned, this creature will talk back instead of answering.', + '***Big Jerk.*** Whenever this creature makes an attack, it starts telling you how much cooler it is than you.', + ]); +}; + +const genLongAbilities = function(){ + return _.sample([ + dedent`***Pack Tactics.*** These guys work together like peanut butter and jelly. Jelly and peanut butter. + + When one of these guys attacks, the target is covered with, well, peanut butter and jelly.`, + dedent`***Hangriness.*** This creature is angry, and hungry. It will refuse to do anything with you until its hunger is satisfied. + + When in visual contact with this creature, you must purchase an extra order of fries, even if they say they aren't hungry.`, + dedent`***Full of Detergent.*** This creature has swallowed an entire bottle of dish detergent and is actually having a pretty good time. + + While walking near this creature, you must make a dexterity check or become "a soapy mess" for three hours, after which your skin will get all dry and itchy.` + ]); +}; + +const genAction = function(){ + const name = _.sample([ + 'Abdominal Drop', + 'Airplane Hammer', + 'Atomic Death Throw', + 'Bulldog Rake', + 'Corkscrew Strike', + 'Crossed Splash', + 'Crossface Suplex', + 'DDT Powerbomb', + 'Dual Cobra Wristlock', + 'Dual Throw', + 'Elbow Hold', + 'Gory Body Sweep', + 'Heel Jawbreaker', + 'Jumping Driver', + 'Open Chin Choke', + 'Scorpion Flurry', + 'Somersault Stump Fists', + 'Suffering Wringer', + 'Super Hip Submission', + 'Super Spin', + 'Team Elbow', + 'Team Foot', + 'Tilt-a-whirl Chin Sleeper', + 'Tilt-a-whirl Eye Takedown', + 'Turnbuckle Roll' + ]); + + return `***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `; +}; + + +module.exports = { + + monster : function(classes, genLines){ + return dedent` + {{${classes} + ## ${getMonsterName()} + *${getType()}, ${getAlignment()}* + ___ + **Armor Class** :: ${_.random(10, 20)} (chain mail, shield) + **Hit Points** :: ${_.random(1, 150)}(1d4 + 5) + **Speed** :: ${_.random(0, 50)}ft. + ___ + | STR | DEX | CON | INT | WIS | CHA | + |:-----:|:-----:|:-----:|:-----:|:-----:|:-----:| + ${getStats()} + ___ + **Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)} + **Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)} + **Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)} + **Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP) + ___ + ${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n:\n')} + : + ${genLongAbilities()} + ### Actions + ${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n:\n')} + }} + \n`; + } +}; diff --git a/themes/V3/5eDMG/snippets/tableOfContents.gen.js b/themes/V3/5eDMG/snippets/tableOfContents.gen.js new file mode 100644 index 000000000..1c52d5cf7 --- /dev/null +++ b/themes/V3/5eDMG/snippets/tableOfContents.gen.js @@ -0,0 +1,84 @@ +const _ = require('lodash'); +const dedent = require('dedent-tabs').default; + +const getTOC = (pages)=>{ + const add1 = (title, page)=>{ + res.push({ + title : title, + page : page + 1, + children : [] + }); + }; + const add2 = (title, page)=>{ + if(!_.last(res)) add1(null, page); + _.last(res).children.push({ + title : title, + page : page + 1, + children : [] + }); + }; + const add3 = (title, page)=>{ + if(!_.last(res)) add1(null, page); + if(!_.last(_.last(res).children)) add2(null, page); + _.last(_.last(res).children).children.push({ + title : title, + page : page + 1, + children : [] + }); + }; + + const res = []; + _.each(pages, (page, pageNum)=>{ + const lines = page.split('\n'); + _.each(lines, (line)=>{ + if(_.startsWith(line, '# ')){ + const title = line.replace('# ', ''); + add1(title, pageNum); + } + if(_.startsWith(line, '## ')){ + const title = line.replace('## ', ''); + add2(title, pageNum); + } + if(_.startsWith(line, '### ')){ + const title = line.replace('### ', ''); + add3(title, pageNum); + } + }); + }); + return res; +}; + +module.exports = function(brew){ + const pages = brew.text.split('\\page'); + const TOC = getTOC(pages); + const markdown = _.reduce(TOC, (r, g1, idx1)=>{ + if(g1.title !== null) { + r.push(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`); + } + if(g1.children.length){ + _.each(g1.children, (g2, idx2)=>{ + if(g2.title !== null) { + r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`); + } + if(g2.children.length){ + _.each(g2.children, (g3, idx3)=>{ + if(g2.title !== null) { + r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`); + } else { // Don't over-indent if no level-2 parent entry + r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`); + } + }); + } + }); + } + return r; + }, []).join('\n'); + + return dedent` + {{toc,wide + # Table Of Contents + + ${markdown} + }} + \n`; +}; diff --git a/themes/V3/5eDMG/snippets/watercolor.gen.js b/themes/V3/5eDMG/snippets/watercolor.gen.js new file mode 100644 index 000000000..735a35602 --- /dev/null +++ b/themes/V3/5eDMG/snippets/watercolor.gen.js @@ -0,0 +1,5 @@ +const _ = require('lodash'); + +module.exports = ()=>{ + return `{{watercolor${_.random(1, 12)},top:20px,left:30px,width:300px,background-color:#BBAD82,opacity:80%}}\n\n`; +}; diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less new file mode 100644 index 000000000..4657828dd --- /dev/null +++ b/themes/V3/5eDMG/style.less @@ -0,0 +1,25 @@ +@import (less) './themes/fonts/5e/fonts.less'; +@import (less) './themes/assets/assets.less'; +@import (less) './themes/V3/5ePHB/style.less'; + +@noteGreen : #ebcec3; // Salmon +@footnotes : #5c5c5c; // Dark gray + +.page { + background-image : url(/assets/DMG_background.png); + background-size : cover; + + &:after { + background-image : url(/assets/DMG_footerAccent.png); + height: 58px; + } + + .pageNumber { + color : @footnotes; + } + + .footnote { + color : @footnotes; + bottom : 40px; + } +} diff --git a/themes/assets/DMG_background.png b/themes/assets/DMG_background.png new file mode 100644 index 000000000..5fbccf9a8 Binary files /dev/null and b/themes/assets/DMG_background.png differ diff --git a/themes/assets/DMG_footerAccent.png b/themes/assets/DMG_footerAccent.png new file mode 100644 index 000000000..cc115709a Binary files /dev/null and b/themes/assets/DMG_footerAccent.png differ diff --git a/themes/assets/footerAccent.png b/themes/assets/PHB_footerAccent.png similarity index 100% rename from themes/assets/footerAccent.png rename to themes/assets/PHB_footerAccent.png diff --git a/themes/assets/assets.less b/themes/assets/assets.less index 8790b1e59..31acfbfad 100644 --- a/themes/assets/assets.less +++ b/themes/assets/assets.less @@ -1,5 +1,5 @@ // PHB -@footerAccentImage : data-uri('./themes/assets/footerAccent.png'); +@footerAccentImage : data-uri('./themes/assets/PHB_footerAccent.png'); @frameBorderImage : data-uri('./themes/assets/frameBorder.png'); @backgroundImage : data-uri('./themes/assets/parchmentBackground.jpg'); @redTriangleImage : data-uri('./themes/assets/redTriangle.png'); diff --git a/themes/themes.json b/themes/themes.json index d8682bac3..14935e598 100644 --- a/themes/themes.json +++ b/themes/themes.json @@ -3,14 +3,22 @@ { "name": "5e PHB", "renderer": "legacy", - "baseTheme": false + "baseTheme": false, + "path": "5ePHB" } ], "V3": [ + { + "name": "5e DMG", + "renderer": "V3", + "baseTheme": "5ePHB", + "path": "5eDMG" + }, { "name": "5e PHB", "renderer": "V3", - "baseTheme": false + "baseTheme": false, + "path": "5ePHB" } ] } \ No newline at end of file