Skip to content

Commit 162ef5a

Browse files
committed
[#6922] Add welcome dialog with links, settings, & module list
Adds a new window that pops up when the system is first launched to provide some first run improvements to users. This window displays a welcome message & several links to documentation & support pages. Beneath that are some important settings that are useful to get set immediately including rules version, calendar, bastion, and whether the metric system is used. Any changes to these settings are saved when the window is closed and a reload will be prompted if needed. The second tab contains a list of all of the first-party modules offered for the system. These modules can be easily enabled from this screen and any selected modules will be enabled when the app is closed. The list of modules is contained in a JSON file included with the system, but it will also check the copy of that file on the `publish-wiki` branch of the GitHub. This should allow the system to add new modules to this listing without requiring a new system version. ### Todo - [ ] Write welcome dialog & links - [ ] Modify compendium browser filters to match selected rules and enabled modules - [ ] Integrate with adventure quickstart dialog #6946 Closes #6922
1 parent 6377a15 commit 162ef5a

25 files changed

Lines changed: 505 additions & 9 deletions

dnd5e.mjs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ Hooks.once("i18nInit", () => {
536536
/**
537537
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
538538
*/
539-
Hooks.once("ready", function() {
539+
Hooks.once("ready", async function() {
540540
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
541541
Hooks.on("hotbarDrop", (bar, data, slot) => {
542542
if ( ["ActiveEffect", "Activity", "Item"].includes(data.type) ) {
@@ -564,8 +564,24 @@ Hooks.once("ready", function() {
564564
dnd5e.ui.calendar.render({ force: true });
565565
}
566566

567-
// Determine whether a system migration is required and feasible
567+
// Run migrations
568+
await _handleMigration();
569+
570+
// Present welcome dialog to GMs
571+
if ( game.user.isGM && game.settings.get("dnd5e", "firstRun") ) {
572+
const welcome = new applications.WelcomeScreen();
573+
welcome.render({ force: true });
574+
}
575+
});
576+
577+
/* -------------------------------------------- */
578+
579+
/**
580+
* Determine whether a system migration is required and feasible and run it.
581+
*/
582+
async function _handleMigration() {
568583
if ( !game.user.isGM ) return;
584+
569585
const cv = game.settings.get("dnd5e", "systemMigrationVersion") || game.world.flags.dnd5e?.version;
570586
const totalDocuments = game.actors.size + game.scenes.size + game.items.size;
571587
if ( !cv && totalDocuments === 0 ) return game.settings.set("dnd5e", "systemMigrationVersion", game.system.version);
@@ -580,8 +596,8 @@ Hooks.once("ready", function() {
580596
if ( cv && foundry.utils.isNewerVersion(game.system.flags.compatibleMigrationVersion, cv) ) {
581597
ui.notifications.error("MIGRATION.5eVersionTooOldWarning", { permanent: true });
582598
}
583-
migrations.migrateWorld();
584-
});
599+
await migrations.migrateWorld();
600+
}
585601

586602
/* -------------------------------------------- */
587603
/* System Styling */

json/official-content.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"core": {
3+
"dnd-players-handbook": {
4+
"name": "Player's Handbook",
5+
"description": "<p>Embark on an epic adventure 10 years in the making. This new and improved Player's Handbook® is the ultimate guide for fifth edition D&D players. Play your way with optimized rules for character creation and advancement, exploration, combat, equipment, spells, and much more. Create fantastic heroes from an expanded selection of character origins, classes, and subclasses, revised and balanced for maximum fun.</p>"
6+
},
7+
"dnd-dungeon-masters-guide": {
8+
"name": "Dungeon Master's Guide",
9+
"description": "<p>Create thrilling adventures with this revised and expanded Dungeon Master's Guide. Inside this essential guide is everything new and experienced Dungeon Masters need to weave epic tales, build fantastical worlds, and inspire memorable moments for your party.</p>"
10+
},
11+
"dnd-monster-manual": {
12+
"name": "Monster Manual",
13+
"description": "<p>Populate your worlds and adventures with the hundreds of monsters provided, and let their stories, illustrations, and easy-to-use stat blocks fuel your D&D® adventures for years to come. Encounter a monstrous menagerie of familiar favorites like dragons, giants, and mind flayers, plus a host of new monsters like the arch-hag, the blob of annihilation, and the vampire nightbringer.</p>"
14+
}
15+
},
16+
"expanded": {
17+
"dnd-tashas-cauldron": {
18+
"name": "Tasha's Cauldron of Everything",
19+
"description": "<p>Natasha the Dark, Iggwilv the Witch Queen, daughter of Baba Yaga or simply the witch named Tasha, well-known for her spells such as Tasha’s hideous laughter, has compiled all the precious lore she has gathered during her illustrious career as an adventurer for the enrichment of all. Her book offers a taste of the great power that awaits D&D players and dungeon masters everywhere! It contains new player subclasses, sidekicks, and rules and tools for Dungeon Masters.</p>"
20+
},
21+
"dnd-heroes-faerun": {
22+
"name": "Forgotten Realms: Heroes of Faerûn",
23+
"description": "<p>Become a hero worthy of the Forgotten Realms and carve out a path of your own in this compendium of player options.</p>"
24+
},
25+
"dnd-forge-artificer": {
26+
"name": "Eberron: Forge of the Artificer",
27+
"description": "<p>TODO</p>"
28+
}
29+
},
30+
"adventures": {
31+
"dnd-tomb-annihilation": {
32+
"name": "Tomb of Annihilation",
33+
"description": "<p>Challenge your players to work together to navigate Chult, solve puzzles, and survive the myriad deadly encounters in the Tomb of the Nine Gods as they investigate the mysterious Death Curse that has overtaken the world.</p>"
34+
},
35+
"dnd-phandelver-below": {
36+
"name": "Phandelver & Below: The Shattered Obelisk",
37+
"description": "<p>Journey to the beloved town of Phandalin, where a malevolent cult threatens to overtake the region. Together with your party, solve mysteries and stamp out growing corruption as you uncover more about the peculiar happenings plaguing the town. Discover what lurks below in this high-fantasy adventure that begins with the familiar story of Lost Mine of Phandelver and then delves into the perilous Underdark.</p>"
38+
},
39+
"dnd-heroes-borderlands": {
40+
"name": "Heroes of the Borderlands",
41+
"description": "<p>Gather your party and prepare for an unforgettable journey! Heroes of the Borderlands is your magical portal into the world of Dungeons &amp; Dragons. Perfect for seasoned adventurers and newcomers alike, this is D&amp;D's largest starter set ever and combines the magic of tabletop storytelling with the fun of playing with your friends online using the power of Foundry Virtual Tabletop!</p>"
42+
},
43+
"dnd-adventures-faerun": {
44+
"name": "Forgotten Realms: Adventures in Faerûn",
45+
"description": "<p>The most expansive take on the Forgotten Realms in Dungeons & Dragons history, Forgotten Realms: Adventures in Faerûn has everything Dungeon Masters need to tackle this timeless fantasy setting, complete with new stories, lore, locations, and encounters - all in one place. Go where your imagination leads in this enormous Forgotten Realms expansion. There is no power too vast, no mountaintop too high, and no possibility too great to leave unexplored.</p>"
46+
}
47+
}
48+
}

lang/en.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5628,6 +5628,32 @@
56285628
"DND5E.WeaponSimpleProficiency": "Simple",
56295629
"DND5E.WeaponSimpleR": "Simple Ranged",
56305630
"DND5E.Weight": "Weight",
5631+
5632+
"DND5E.WELCOME": {
5633+
"Button": "Welcome Screen",
5634+
"Module": {
5635+
"Enabled": "Enabled",
5636+
"NotInstalled": "Not Installed"
5637+
},
5638+
"Section": {
5639+
"adventures": "Adventures",
5640+
"core": "Core Rulebooks",
5641+
"expanded": "Expanded Rulebooks"
5642+
},
5643+
"Settings": {
5644+
"Label": "Settings",
5645+
"Metric": {
5646+
"Hint": "Swap to metric units as default for distances, weights, and volume.",
5647+
"Label": "Use Metric System"
5648+
}
5649+
},
5650+
"Tab": {
5651+
"main": "Welcome",
5652+
"modules": "Official Content"
5653+
},
5654+
"Title": "Welcome to D&D 5e"
5655+
},
5656+
56315657
"DND5E.WhisperedTo": "Whispered to",
56325658
"DND5E.Wiki": "Wiki",
56335659
"DND5E.available": "available",

less/dnd5e.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
@import "v2/journal.less";
3636
@import "v2/region-behaviors.less";
3737
@import "v2/blocks.less";
38+
@import "v2/welcome.less";
3839

3940
/* High Contrast */
4041
@import "v2/high-contrast/apps.less";

less/v2/welcome.less

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.welcome.application {
2+
[data-application-part="main"] {
3+
.hero {
4+
box-shadow: 0 3px 6px var(--dnd5e-shadow-45);
5+
}
6+
.message {
7+
font-size: var(--font-size-14);
8+
}
9+
}
10+
11+
[data-application-part="modules"] {
12+
h3 {
13+
margin-block-start: 12px;
14+
margin-block-end: 6px;
15+
border: none;
16+
font-family: var(--dnd5e-font-roboto-slab);
17+
}
18+
h4 {
19+
margin: 0;
20+
font-family: var(--dnd5e-font-roboto-condensed);
21+
font-size: var(--font-size-20);
22+
font-variant: small-caps;
23+
line-height: 1;
24+
}
25+
.key-art {
26+
align-content: center;
27+
align-self: start;
28+
box-shadow: 0 3px 6px var(--dnd5e-shadow-15);
29+
max-inline-size: 240px;
30+
text-align: center;
31+
}
32+
.status {
33+
display: flex;
34+
align-items: center;
35+
justify-content: flex-end;
36+
font-weight: bold;
37+
}
38+
.module-list {
39+
gap: 10px;
40+
41+
.module-entry {
42+
align-items: stretch;
43+
gap: 8px;
44+
}
45+
.module-content {
46+
display: grid;
47+
grid-template-rows: min-content auto min-content;
48+
}
49+
.status { gap: 8px; }
50+
}
51+
}
52+
}

module/applications/_module.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export {default as PropertyAttribution} from "./property-attribution.mjs";
2424
export {default as RollTableSheet5e} from "./roll-table-sheet.mjs";
2525
export {default as Tabs5e} from "./tabs.mjs";
2626
export {PrototypeTokenConfig5e, TokenConfig5e} from "./token-config.mjs";
27+
export {default as WelcomeScreen} from "./welcome-screen.mjs";
2728

2829
// Deprecated
2930
export {default as SheetConfig} from "./sheet-config.mjs";

module/applications/_types.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,11 @@
8989
* @property {number} value Value of this step.
9090
* @property {ActiveEffect5e} [document] Active effect applying this attribution, if any.
9191
*/
92+
93+
/* -------------------------------------------- */
94+
95+
/**
96+
* @typedef OfficialModuleListing
97+
* @property {string} name Display name in the interface.
98+
* @property {string} description Brief description in HTML.
99+
*/

module/applications/components/checkbox.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ export default class CheckboxElement extends AdoptedStyleSheetMixin(
237237
/** @override */
238238
_onClick(event) {
239239
event.preventDefault();
240-
this.checked = !this.checked;
240+
if ( this.indeterminate ) this.indeterminate = false;
241+
else this.checked = !this.checked;
241242
this.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
242243
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
243244
}

module/applications/settings/base-settings.mjs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ export default class BaseSettingsConfig extends Application5e {
5252
* @returns {object}
5353
*/
5454
createSettingField(name) {
55+
return BaseSettingsConfig.createSettingField(name);
56+
}
57+
58+
/* -------------------------------------------- */
59+
60+
/**
61+
* Create the field data for a specific setting.
62+
* @param {string} name Setting key within the dnd5e namespace.
63+
* @returns {object}
64+
*/
65+
static createSettingField(name) {
5566
const setting = game.settings.settings.get(`dnd5e.${name}`);
5667
if ( !setting ) throw new Error(`Setting \`dnd5e.${name}\` not registered.`);
5768
const isDataField = setting.type instanceof DataField;
@@ -86,9 +97,24 @@ export default class BaseSettingsConfig extends Application5e {
8697
* @returns {Promise<void>} Resolves once the settings are updated, or prompts for a reload if required.
8798
*/
8899
static async #onCommitChanges(event, form, formData) {
100+
const changes = foundry.utils.expandObject(formData.object);
101+
const { requiresClientReload, requiresWorldReload } = await BaseSettingsConfig.commitChanges(changes);
102+
if ( requiresClientReload || requiresWorldReload ) {
103+
return foundry.applications.settings.SettingsConfig.reloadConfirm({ world: requiresWorldReload });
104+
}
105+
}
106+
107+
/* -------------------------------------------- */
108+
109+
/**
110+
* Commit changes to various `dnd5e` settings and determine if a reload is required.
111+
* @param {object} changes Changes to apply to settings within the dnd5e namespace.
112+
* @returns {{ requiresClientReload: boolean, requiresWorldReload: boolean }}
113+
*/
114+
static async commitChanges(changes) {
89115
let requiresClientReload = false;
90116
let requiresWorldReload = false;
91-
for ( const [key, value] of Object.entries(foundry.utils.expandObject(formData.object)) ) {
117+
for ( const [key, value] of Object.entries(changes) ) {
92118
const setting = game.settings.settings.get(`dnd5e.${key}`);
93119
const current = game.settings.get("dnd5e", key, { document: true });
94120
const prior = current?._source?.value ?? current;
@@ -97,8 +123,6 @@ export default class BaseSettingsConfig extends Application5e {
97123
requiresClientReload ||= (setting.scope !== "world") && setting.requiresReload;
98124
requiresWorldReload ||= (setting.scope === "world") && setting.requiresReload;
99125
}
100-
if ( requiresClientReload || requiresWorldReload ) {
101-
return foundry.applications.settings.SettingsConfig.reloadConfirm({ world: requiresWorldReload });
102-
}
126+
return { requiresClientReload, requiresWorldReload };
103127
}
104128
}

module/applications/settings/sidebar.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import WelcomeScreen from "../welcome-screen.mjs";
2+
13
/**
24
* Generate sidebar links.
35
* @returns {HTMLUListElement}
@@ -49,4 +51,10 @@ export function renderSettings(html) {
4951
section.append(_generateLinks());
5052
if ( pip ) section.querySelector(".system-info").insertAdjacentElement("beforeend", pip);
5153
html.querySelector(".info").insertAdjacentElement("afterend", section);
54+
55+
const welcomeLink = document.createElement("button");
56+
welcomeLink.type = "button";
57+
welcomeLink.innerHTML = `<i class="fa-solid fa-handshake"></i> ${game.i18n.localize("DND5E.WELCOME.Button")}`;
58+
welcomeLink.addEventListener("click", () => new WelcomeScreen().render({ force: true }));
59+
section.after(welcomeLink);
5260
}

0 commit comments

Comments
 (0)