Skip to content

Commit

Permalink
Merge pull request #1015 from ben/ds2-oracle-tree
Browse files Browse the repository at this point in the history
DS2 oracle trees
  • Loading branch information
ben authored Aug 23, 2024
2 parents ac59ffd + 4ef4d2f commit c40f6d7
Show file tree
Hide file tree
Showing 13 changed files with 498 additions and 279 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Next Release

This is a major update that includes Sundered Isles content, but also brings along a host of changes:

- Selection of content happens in the first-start dialog, which will show the first time you open your world after this update.
- For module/macro authors: registration of custom oracle content has a name change: `'ironsworn'` has changed to `'classic'`, and there's a new `'sundered_isles'` tree.

## 1.23.7

- Fix Cinder and Wraith dice with dice-so-nice 5 ([#1009](https://github.com/ben/foundry-ironsworn/pull/1009))
Expand Down
105 changes: 47 additions & 58 deletions src/module/applications/firstStartDialog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IronswornSettings } from '../helpers/settings'
import { IronswornSettings, RULESETS } from '../helpers/settings'
import { SFSettingTruthsDialogVue } from './vueSfSettingTruthsDialog'
import { WorldTruthsDialog } from './worldTruthsDialog'

Expand All @@ -8,14 +8,15 @@ export class FirstStartDialog extends FormApplication<FormApplicationOptions> {
}

static get defaultOptions(): FormApplicationOptions {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return foundry.utils.mergeObject(super.defaultOptions, {
title: game.i18n.localize('IRONSWORN.First Start.Welcome'),
template: 'systems/foundry-ironsworn/templates/first-start.hbs',
id: 'first-start-dialog',
resizable: false,
classes: ['ironsworn', 'sheet', 'first-start'],
width: 650,
height: 360
height: 560
} as FormApplicationOptions)
}

Expand All @@ -25,75 +26,63 @@ export class FirstStartDialog extends FormApplication<FormApplicationOptions> {

activateListeners(html: JQuery) {
super.activateListeners(html)
html.find('#select-ironsworn').on('click', async (ev) => {
await this._selectIronsworn.call(this, ev)
})
html.find('#select-starforged').on('click', async (ev) => {
await this._selectStarforged.call(this, ev)
})
html.find('#select-sunderedisles').on('click', async (ev) => {
await this._selectSunderedIsles.call(this, ev)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
html.find('button.ironsworn__save').on('click', async (ev) => {
console.log(html)

// Update default character sheet
const defaultSheet = html.find('input[name=sheet]:checked').val()
const setting = game.settings.get('core', 'sheetClasses')
foundry.utils.mergeObject(setting, {
'Actor.character': `ironsworn.${defaultSheet}`
})
await game.settings.set('core', 'sheetClasses', setting)

// Update rulesets
const checkedRulesets: any[] = $.map(
html.find('input.ruleset:checked'),
(x: any) => x.value ?? ''
)
await IronswornSettings.enableOnlyRulesets(...checkedRulesets)

if (IronswornSettings.get('show-first-start-dialog')) {
if (defaultSheet === 'StarforgedCharacterSheet') {
void new SFSettingTruthsDialogVue().render(true)
} else {
void new WorldTruthsDialog().render(true)
}
}
await IronswornSettings.set('show-first-start-dialog', false)
})
}

async getData(_options?: unknown) {
const rulesets = {}
for (const r of RULESETS) {
rulesets[r] = {
id: r,
name: game.i18n.localize(`IRONSWORN.RULESETS.${r}`),
enabled: IronswornSettings.get(`ruleset-${r}`)
}
}
return {
...(await super.getData()),
sunderedislesBeta: IronswornSettings.get('sundered-isles-beta')
rulesets: rulesets,
sheetIsIronsworn: this.currentDefaultSheet === 'ironsworn',
sheetIsStarforged: this.currentDefaultSheet === 'starforged'
}
}

async _selectIronsworn(ev) {
ev.preventDefault()

// Character sheet
const setting = game.settings.get('core', 'sheetClasses')
foundry.utils.mergeObject(setting, {
'Actor.character': 'ironsworn.IronswornCharacterSheetV2'
})
await game.settings.set('core', 'sheetClasses', setting)

// Truths
new WorldTruthsDialog().render(true)
game.settings.set('foundry-ironsworn', 'prompt-world-truths', false)
this.close()
}

async _selectStarforged(ev) {
ev.preventDefault()

// Character sheet
const setting = game.settings.get('core', 'sheetClasses')
foundry.utils.mergeObject(setting, {
'Actor.character': 'ironsworn.StarforgedCharacterSheet'
})
await game.settings.set('core', 'sheetClasses', setting)

// Truths
new SFSettingTruthsDialogVue().render(true)
game.settings.set('foundry-ironsworn', 'prompt-world-truths', false)
this.close()
}

async _selectSunderedIsles(ev) {
ev.preventDefault()

// Use the Starforged character sheet
get currentDefaultSheet(): 'ironsworn' | 'starforged' {
const setting = game.settings.get('core', 'sheetClasses')
foundry.utils.mergeObject(setting, {
'Actor.character': 'ironsworn.StarforgedCharacterSheet'
})
await game.settings.set('core', 'sheetClasses', setting)

// TODO: Sundered Isles Truths
// new SFSettingTruthsDialogVue().render(true)
// game.settings.set('foundry-ironsworn', 'prompt-world-truths', false)

this.close()
if (setting.Actor?.character === 'ironsworn.StarforgedCharacterSheet') {
return 'starforged'
}
return 'ironsworn'
}

static async maybeShow() {
if (!IronswornSettings.get('prompt-world-truths')) {
if (!IronswornSettings.get('show-first-start-dialog')) {
return
}

Expand Down
177 changes: 91 additions & 86 deletions src/module/features/customoracles.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,35 @@
import type {
IOracle,
IOracleCategory,
Starforged,
Ironsworn
} from 'dataforged'
import { starforged, ironsworn } from 'dataforged'
OracleCollection,
OracleRollableTable,
OracleTablesCollection,
RulesetId
} from '@datasworn/core/dist/Datasworn'
import type { IOracle, IOracleCategory } from 'dataforged'
import { cloneDeep, compact } from 'lodash-es'
import { OracleTable } from '../roll-table/oracle-table'
import { cachedDocumentsForPack } from './pack-cache'
import { DataswornTree } from '../datasworn2'
import { IronswornSettings } from '../helpers/settings'
import {
DS_ORACLE_COMPENDIUM_KEYS,
OracleTable
} from '../roll-table/oracle-table'

export interface IOracleTreeNode {
dataforgedNode?: IOracle | IOracleCategory
dataswornNode?: OracleCollection | OracleRollableTable
tables: string[] // UUIDs
displayName: string
children: IOracleTreeNode[]
forceExpanded?: boolean
forceHidden?: boolean
}

// For some reason, rollupJs mangles this
const SFOracleCategories = ((starforged as any).default as Starforged)[
'Oracle Categories'
]
const ISOracleCategories = ((ironsworn as any).default as Ironsworn)[
'Oracle Categories'
]

const emptyNode: () => IOracleTreeNode = () => ({
displayName: '',
tables: [],
children: []
})

async function createOracleTree(
compendium: string,
categories: IOracleCategory[]
): Promise<IOracleTreeNode> {
const rootNode = emptyNode()

// Make sure the compendium is loaded
await cachedDocumentsForPack(compendium)

// Build the default tree
for (const category of categories) {
rootNode.children.push(await walkOracleCategory(category))
}

return rootNode
}

async function createIronswornOracleTree(): Promise<IOracleTreeNode> {
return await createOracleTree(
'foundry-ironsworn.ironswornoracles',
ISOracleCategories
)
}

async function createStarforgedOracleTree(): Promise<IOracleTreeNode> {
return await createOracleTree(
'foundry-ironsworn.starforgedoracles',
SFOracleCategories
)
}

async function walkOracleCategory(
cat: IOracleCategory
): Promise<IOracleTreeNode> {
const node: IOracleTreeNode = {
...emptyNode(),
dataforgedNode: cat,
displayName: game.i18n.localize(`IRONSWORN.OracleCategories.${cat.Name}`)
}

for (const childCat of cat.Categories ?? []) {
node.children.push(await walkOracleCategory(childCat))
}
for (const oracle of cat.Oracles ?? []) {
node.children.push(await walkOracle(oracle))
}

// Promote children of nodes that have a table
for (const child of node.children) {
if (child.tables.length > 0) {
node.children = [...node.children, ...child.children]
child.children = []
}
}

return node
}

// TODO: only used for sf-moverow now, clean that up
export async function walkOracle(
oracle?: IOracle | IOracleCategory
): Promise<IOracleTreeNode> {
Expand Down Expand Up @@ -206,13 +146,9 @@ export function findPathToNodeByDfId(rootNode: IOracleTreeNode, dfid: string) {
return ret
}

type OracleCategory = 'ironsworn' | 'starforged' | 'sunderedisles'
type OracleCategory = 'classic' | 'delve' | 'starforged' | 'sundered_isles'

const ORACLES: Record<OracleCategory, IOracleTreeNode> = {
ironsworn: emptyNode(),
starforged: emptyNode(),
sunderedisles: emptyNode()
}
const ORACLES: Record<string, IOracleTreeNode> = {}

export function registerOracleTreeInternal(
category: OracleCategory,
Expand All @@ -223,14 +159,67 @@ export function registerOracleTreeInternal(

let defaultTreesInitialized = false

export async function registerDefaultOracleTrees() {
const ironswornOracles = await createIronswornOracleTree()
registerOracleTreeInternal('ironsworn', ironswornOracles)
function walkOracleTable(
node: OracleRollableTable,
index: any
): IOracleTreeNode {
const tableIndexEntry = index.contents.find(
(x) => x.flags?.['foundry-ironsworn']?.['dsid'] === node._id
)
return {
dataswornNode: node,
displayName: tableIndexEntry?.name ?? node.name,
tables: compact([tableIndexEntry?.uuid]),
children: []
}
}

const starforgedOracles = await createStarforgedOracleTree()
registerOracleTreeInternal('starforged', starforgedOracles)
function walkDsOracleCollection(
node: OracleTablesCollection,
index: any
): IOracleTreeNode {
const children = [
...Object.values(node?.collections ?? {}).map((x) =>
walkDsOracleCollection(x as OracleTablesCollection, index)
),
...Object.values(node?.contents ?? {}).map((x) => walkOracleTable(x, index))
]
if (
game.i18n
.localize(`IRONSWORN.OracleCategories.${node.name}`)
.startsWith('IRONSWORN')
) {
console.log('!!!', node.name)
}
return {
dataswornNode: node,
displayName: game.i18n.localize(`IRONSWORN.OracleCategories.${node.name}`),
tables: [],
children
}
}

// TODO: Sundered Isles oracles
async function generateTreeFromDsData(
ruleset: RulesetId
): Promise<IOracleTreeNode> {
const pack = game.packs.get(DS_ORACLE_COMPENDIUM_KEYS[ruleset])
const index = await pack.getIndex({ fields: ['flags'] })

const rp = DataswornTree.get(ruleset)
return {
displayName: game.i18n.localize(`IRONSWORN.RULESETS.${ruleset}`),
tables: [],
children: Object.values(rp?.oracles ?? {}).map((o) =>
walkDsOracleCollection(o, index)
)
}
}

export async function registerDefaultOracleTrees() {
for (const ruleset of IronswornSettings.enabledRulesets) {
const tree = await generateTreeFromDsData(ruleset)
registerOracleTreeInternal(ruleset, tree)
}

defaultTreesInitialized = true
Hooks.call('ironswornOracleTreesReady')
Expand Down Expand Up @@ -270,3 +259,19 @@ export async function getOracleTreeWithCustomOracles(

return rootNode
}

export async function getCustomizedOracleTrees(): Promise<IOracleTreeNode[]> {
return await Promise.all(
Object.keys(ORACLES).map(async (ruleset) => {
const tree = ORACLES[ruleset]

// Add in custom oracles from a well-known directory
await augmentWithFolderContents(tree)

// Fire the hook and allow extensions to modify the tree
Hooks.call('ironswornOracles', tree)

return tree
})
)
}
Loading

0 comments on commit c40f6d7

Please sign in to comment.