Skip to content

Commit

Permalink
Merge pull request #330 from ben/oracle-hook
Browse files Browse the repository at this point in the history
Oracle hook
  • Loading branch information
ben authored May 1, 2022
2 parents 4a7218f + 2b54c4d commit bd01978
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 75 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## In progress

- Add the "path" to the rolled oracle to the chat card output ([#329](https://github.com/ben/foundry-ironsworn/pull/329))
- Allow custom oracles ([#330](https://github.com/ben/foundry-ironsworn/pull/330))

## 1.10.57

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ Assets are in their own compendium, drag them onto your character sheet.
Now you're ready to create vows, embark on journeys, and slay beasts.
Keep track of your story using journal entries.

## Extensibility

**Custom oracles** can be added in one of two ways.
The first is by creating a table folder called "Custom Oracles" (this name may be translated).
Whatever folder and table structure is found there will be mirrored into the oracle sidebar.

The second is more useful for modules, and involves intercepting the `ironswornOracles` hook.
The type of object in the tree is shown in `customoracles.ts`.

## How to hack on this

1. Install Foundry 0.8.8 or later, and start it up.
Expand Down
111 changes: 111 additions & 0 deletions src/module/features/customoracles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { starforged, IOracle, IOracleCategory } from 'dataforged'
import { getFoundryTableByDfId } from '../dataforged'

export interface OracleTreeNode {
dataforgedNode?: IOracle | IOracleCategory
table?: RollTable
displayName: string
children: OracleTreeNode[]
}

const emptyNode = () =>
({
displayName: '',
children: [],
} as OracleTreeNode)

export async function createStarforgedOracleTree(): Promise<OracleTreeNode> {
const rootNode = emptyNode()

// Make sure the compendium is loaded
const pack = game.packs.get('foundry-ironsworn.starforgedoracles')
await pack?.getDocuments()

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

// TODO: Add in custom oracles from a well-known directory
await augmentWithFolderContents(rootNode)

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

return rootNode
}

async function walkOracleCategory(cat: IOracleCategory): Promise<OracleTreeNode> {
const node: OracleTreeNode = {
...emptyNode(),
dataforgedNode: cat,
displayName: game.i18n.localize(`IRONSWORN.SFOracleCategories.${cat.Display.Title}`),
}

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

return node
}

async function walkOracle(oracle: IOracle): Promise<OracleTreeNode> {
const table = await getFoundryTableByDfId(oracle.$id)

const node: OracleTreeNode = {
...emptyNode(),
dataforgedNode: oracle,
table,
displayName: table?.name || game.i18n.localize(`IRONSWORN.SFOracleCategories.${oracle.Display.Title}`),
}

for (const childOracle of oracle.Oracles ?? []) node.children.push(await walkOracle(childOracle))

return node
}

async function augmentWithFolderContents(node:OracleTreeNode) {
const name = game.i18n.localize('IRONSWORN.Custom Oracles')
const folder = game.tables?.directory?.folders.find(x => x.name === name)
if (!folder) return

function walkFolder(parent:OracleTreeNode, folder: Folder) {
// Add this folder
const newNode:OracleTreeNode = {
...emptyNode(),
displayName: folder.name || '(folder)'
}
parent.children.push(newNode)

// Add its folder children
for (const sub of folder.getSubfolders()) {
walkFolder(newNode, sub)
}

// Add its table children
for (const table of folder.contents) {
newNode.children.push({
...emptyNode(),
table,
displayName: table.name ?? '(table)',
})
}
}

walkFolder(node, folder)
}

export function findPathToNodeByTableId(rootNode: OracleTreeNode, tableId: string): OracleTreeNode[] {
const ret: OracleTreeNode[] = []
function walk(node:OracleTreeNode) {
ret.push(node)
if (node.table?.id === tableId) return true
for (const child of node.children) {
if (walk(child)) return true
}
ret.pop()
return false
}

walk(rootNode)
return ret
}
48 changes: 14 additions & 34 deletions src/module/vue/components/oracletree-node.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<div class="flexcol nogrow" :class="{ hidden: hidden }">
<!-- TODO: split this into two components, yo -->
<!-- Leaf node -->
<div v-if="oracle.foundryTable">
<div v-if="node.table">
<h4 class="clickable text flexrow">
<span @click="rollOracle">
<i class="isicon-d10-tilt juicy"></i>
{{ name }}
{{ node.displayName }}
</span>
<icon-button
v-if="oracle.foundryTable"
v-if="node.table"
icon="eye"
@click="descriptionExpanded = !descriptionExpanded"
/>
Expand Down Expand Up @@ -38,16 +38,16 @@
<i v-if="expanded" class="fa fa-caret-down" />
<i v-else class="fa fa-caret-right" />
</span>
{{ name }}
{{ node.displayName }}
</h4>

<transition name="slide">
<div class="flexcol" v-if="expanded" style="margin-left: 1rem">
<oracletree-node
v-for="child in children"
:key="child.dfid"
v-for="child in node.children"
:key="child.displayName"
:actor="actor"
:oracle="child"
:node="child"
:searchQuery="searchQuery"
:parentMatchesSearch="matchesSearch"
ref="children"
Expand Down Expand Up @@ -75,7 +75,7 @@ h4 {
export default {
props: {
actor: Object,
oracle: Object,
node: Object,
searchQuery: String,
parentMatchesSearch: Boolean,
},
Expand All @@ -88,44 +88,32 @@ export default {
},
computed: {
children() {
return [...(this.oracle.Categories ?? []), ...(this.oracle.Oracles ?? [])]
},
childMatchesSearch() {
this.searchQuery
return this.$refs.children?.find((x) => x.matchesSearch)
},
expanded() {
return this.manuallyExpanded || !!this.searchQuery
},
name() {
return (
this.oracle.foundryTable?.name ??
this.$t(`IRONSWORN.SFOracleCategories.${this.oracle.Display.Title}`)
)
},
matchesSearch() {
const re = new RegExp(this.searchQuery, 'i')
return re.test(this.name)
return re.test(this.node.displayName)
},
hidden() {
if (!this.searchQuery) return false
return !(
this.matchesSearch || // This matches
this.parentMatchesSearch || // Parent matches
this.matchesSearch ||
this.parentMatchesSearch ||
this.childMatchesSearch
)
},
tablePreview() {
const description = this.oracle.foundryTable.data.description || ''
const description = this.node.table.data.description || ''
const tableRows = CONFIG.IRONSWORN._.sortBy(
this.oracle.foundryTable.data.results.contents.map((x) => ({
this.node.table.data.results.contents.map((x) => ({
low: x.data.range[0],
high: x.data.range[1],
text: x.data.text,
Expand All @@ -144,16 +132,8 @@ export default {
},
methods: {
click() {
if (this.oracle.foundryTable) {
CONFIG.IRONSWORN.rollAndDisplayOracleResult(this.oracle.foundryTable)
} else {
this.manuallyExpanded = !this.manuallyExpanded
}
},
rollOracle() {
CONFIG.IRONSWORN.rollAndDisplayOracleResult(this.oracle.foundryTable)
CONFIG.IRONSWORN.rollAndDisplayOracleResult(this.node.table)
},
moveclick(item) {
Expand Down
47 changes: 6 additions & 41 deletions src/module/vue/components/sf-movesheetoracles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@

<div class="flexcol item-list">
<oracletree-node
v-for="oracle in dfOracles"
:key="oracle.key"
v-for="node in treeRoot.children"
:key="node.displayName"
:actor="actor"
:oracle="oracle"
:node="node"
:searchQuery="checkedSearchQuery"
ref="oracles"
/>
Expand All @@ -38,44 +38,22 @@
</style>

<script>
import { cloneDeep } from 'lodash'
import { createStarforgedOracleTree } from '../../features/customoracles'
export default {
props: {
actor: Object,
},
data() {
const dfOracles = CONFIG.IRONSWORN.dataforgedHelpers.cleanDollars(
cloneDeep(CONFIG.IRONSWORN.Dataforged.oracles)
)
return {
dfOracles,
flatOracles: [],
searchQuery: '',
treeRoot: {children: []},
}
},
async created() {
// Make sure all the oracles are loaded
await this.getPack().getDocuments()
// Walk the DF oracles and decorate with Foundry IDs
const walk = async (node) => {
const table =
await CONFIG.IRONSWORN.dataforgedHelpers.getFoundryTableByDfId(
node.dfid
)
if (table) {
Vue.set(node, 'foundryTable', table)
this.flatOracles.push(node)
}
for (const child of node.Oracles ?? []) await walk(child)
for (const child of node.Categories ?? []) await walk(child)
}
for (const node of this.dfOracles) await walk(node)
this.treeRoot = await createStarforgedOracleTree()
},
computed: {
Expand All @@ -87,22 +65,9 @@ export default {
return ''
}
},
searchResults() {
if (!this.searchQuery) return null
const re = new RegExp(this.checkedSearchQuery, 'i')
return this.flatOracles.filter((x) =>
re.test(`${x.Category}/${x.foundryTable.name}`)
)
},
},
methods: {
getPack() {
return game.packs.get('foundry-ironsworn.starforgedoracles')
},
clearSearch() {
this.searchQuery = ''
},
Expand Down
1 change: 1 addition & 0 deletions system/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"Custom Moves": "Custom Moves",
"CustomMoves": "Custom Moves",
"Oracles": "Oracles",
"Custom Oracles": "Custom Oracles",
"Search": "Search",
"Plot": "Plot",
"Location": "Location",
Expand Down

0 comments on commit bd01978

Please sign in to comment.