Skip to content

Commit

Permalink
Persist Challenge Trackers
Browse files Browse the repository at this point in the history
- Persist challenge trackers across sessions using user flags.
- Add forms to manage challenge trackers.
- Add button on player list to open forms.
  • Loading branch information
Larkinabout committed Jul 19, 2022
1 parent bbff71a commit b7a84d2
Show file tree
Hide file tree
Showing 14 changed files with 819 additions and 199 deletions.
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Challenge Tracker
An interactive aid to track successes and failures in challenges.
An interactive aid to track successes and failures in challenges à la D&D 4e-inspired skill challenges and Blades in the Dark progress clocks.

![challenge-tracker](./images/challenge-tracker.png) ![challenge-tracker](./images/challenge-tracker-progress-clock.png)

Expand All @@ -10,22 +10,36 @@ An interactive aid to track successes and failures in challenges.
- **Player View:** Click **Show** on the header to show the tracker to other players and click **Hide** to hide it from other players.

## How to Use
### Using the Player List
![challenge-tracker-macro](./images/challenge-tracker-player-list.png)
1. Click the button ![challenge-tracker-macro](./images/challenge-tracker-player-list-button.png) in the player list.

![challenge-tracker-macro](./images/challenge-tracker-list.png)

2. Click 'Create New' to create a new Challenge Tracker.
3. Fill in the options and click 'Save and Close'.
- Click 'Open' to open a Challenge Tracker.
- Click 'Edit' to edit an existing Challenge Tracker.
- Click 'Delete' to delete an existing Challenge Tracker.

### Using Macros
1. Create a macro with a Type of 'script' and enter: `ChallengeTracker.open(outer, inner)` where `outer` is the number of segments required on the outer ring (successes) and `inner` is the number of segments required on the inner circle (failures).
2. Execute the macro to open the Challenge Tracker.

![challenge-tracker-macro](./images/challenge-tracker-macro.png)

## Advanced Options
More options can be set using an optional array parameter: `ChallengeTracker.open(successes failures, {options})` where options is a comma-separated list of any of the following parameters in the format `option: value`:
- **show:** Set to `true` to show the Challenge Tracker to your players. Default is `false`. Example: `show: true`
- **outerCurrent:** Set the number of completed segments on the outer ring (successes). Default is `0`. Example: `outerCurrent: 3`
- **innerCurrent:** Set the number of completed segments on the inner circle (failures). Default is `0`. Example: `innerCurrent: 3`
- **outerCurrent:** Set the number of completed segments on the outer ring (successes). Default is `0`. Example: `outerCurrent: 3
- **innerCurrent:** Set the number of completed segments on the inner circle (failures). Default is `0`. Example: `innerCurrent:
- **outerColor:** Set the hex color of the outer ring (successes). The 'Outer Color' module setting will be ignored. Example: `outerColor: '#0000FF'`
- **innerColor:** Set the hex color of the inner circle (failures). The 'Inner Color' module setting will be ignored. Example: `innerColor: '#0000FF'`
- **frameColor:** Set the hex color of the frame. The 'Frame Color' module setting will be ignored. Example: `frameColor: '#0000FF'`
- **persist:** Set to `true` to persist the Challenge Tracker across sessions. Default is `false`. Example: `persist: true`
- **show:** Set to `true` to show the Challenge Tracker to your players. Default is `false`. Example: `show: true`
- **size:** Set the size of the Challenge Tracker in pixels between 200 to 600. The 'Size' module setting will be ignored. Example: `size: 400`
- **windowed:** Set the Challenge Tracker to windowed (true) or windowless (false). The 'Windowed' module setting will be ignored. Example: `windowed: false`
- **title:** Set the title of the Challenge Tracker in the window header. Default is `Challenge Tracker`. Example: `title: 'Skill Challenge 1'`
- **windowed:** Set the Challenge Tracker to windowed (true) or windowless (false). The 'Windowed' module setting will be ignored. Example: `windowed: false`

## Examples
### Progress Clock
Expand Down
Binary file added images/challenge-tracker-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/challenge-tracker-player-list-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/challenge-tracker-player-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 39 additions & 33 deletions languages/en.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
{
"settings": {
"allowShow" : {
"name": "Show to Others",
"hint": "Allow users with this role (and above) to show Challenge Trackers to others (Requires reload)"
},
"outerColor" : {
"name": "Outer Color",
"hint": "Set the default color for the outer ring",
"label": "Color Picker"
},
"innerColor" : {
"name": "Inner Color",
"hint": "Set the default color for the inner circle",
"label": "Color Picker"
},
"frameColor" : {
"name": "Frame Color",
"hint": "Set the default color of the frame",
"label": "Color Picker"
},
"size" : {
"name": "Size",
"hint": "Set the default size of the challenge tracker in pixels"
},
"windowed" : {
"name": "Windowed",
"hint": "Set the challenge tracker to windowed by default"
},
"scroll" : {
"name": "Scroll",
"hint": "Enable the scroll wheel for increasing/decreasing segments"
}
}
"challengeTracker": {
"labels": {
"challengeTrackerTitle": "Challenge Tracker",
"challengeTrackerButtonTitle": "Challenge Tracker"
},
"settings": {
"allowShow" : {
"name": "Show to Others",
"hint": "Allow users with this role (and above) to show Challenge Trackers to others (Requires reload)"
},
"outerColor" : {
"name": "Outer Color",
"hint": "Set the default color for the outer ring",
"label": "Color Picker"
},
"innerColor" : {
"name": "Inner Color",
"hint": "Set the default color for the inner circle",
"label": "Color Picker"
},
"frameColor" : {
"name": "Frame Color",
"hint": "Set the default color of the frame",
"label": "Color Picker"
},
"size" : {
"name": "Size",
"hint": "Set the default size of the challenge tracker in pixels"
},
"windowed" : {
"name": "Windowed",
"hint": "Set the challenge tracker to windowed by default"
},
"scroll" : {
"name": "Scroll",
"hint": "Enable the scroll wheel for increasing/decreasing segments"
}
}
}
}
4 changes: 2 additions & 2 deletions module.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "challenge-tracker",
"title": "Challenge Tracker",
"description": "An interactive aid to track successes and failures in challenges",
"version": "0.6",
"description": "An interactive aid to track successes and failures in challenges à la D&D 4e-inspired skill challenges and Blades in the Dark progress clocks",
"version": "0.7",
"library": "false",
"manifestPlusVersion": "1.0.0",
"minimumCoreVersion": "9",
Expand Down
44 changes: 43 additions & 1 deletion scripts/challenge-tracker.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { ChallengeTracker } from './main.js'
import { Utils } from './utils.js'
import { Settings } from './settings.js'
import { ChallengeTrackerForm, ChallengeTrackerEditForm } from './form.js'
import { ChallengeTrackerFlag } from './flags.js'

Hooks.once('init', () => {
Settings.init()
ChallengeTrackerForm.init()
ChallengeTrackerEditForm.init()
Handlebars.registerHelper('ifEquals', function (arg1, arg2, options) {
return (arg1 === arg2) ? options.fn(this) : options.inverse(this)
})
})

Hooks.once('colorSettingsInitialized', async () => {
Expand Down Expand Up @@ -38,11 +45,46 @@ Hooks.once('ready', async () => {
}
})

/* Add buttons to the Player List */
Hooks.on('renderPlayerList', (playerList, html) => {
const tooltip = game.i18n.localize('challengeTracker.labels.challengeTrackerButtonTitle')
const svg = `<svg width="100" height="100" viewBox="-5 -5 110 110" xmlns="http://www.w3.org/2000/svg">
<title>challenge-tracker-button-icon</title>
<ellipse stroke-width="7" id="outer_circle" cx="50" cy="50" rx="50" ry="50" stroke="currentColor" fill="none" fill-opacity="0"/>
<ellipse stroke-width="7" id="inner_circle" cx="50" cy="50" rx="30" ry="30" stroke="currentColor" fill="none" fill-opacity="0"/>
<line stroke-width="7" id="svg_4" x1="50" x2="50" y1="0" y2="50" stroke="currentColor" fill="none" fill-opacity="0"/>
<line stroke-width="7" id="svg_5" x1="50" x2="76" y1="50" y2="65" stroke="currentColor" fill="none" fill-opacity="0"/>
<line stroke-width="7" id="svg_6" x1="50" x2="24" y1="50" y2="65" stroke="currentColor" fill="none" fill-opacity="0"/>
<line stroke-width="7" id="svg_8" x1="50" x2="50" y1="80" y2="100" stroke="currentColor" fill="none" fill-opacity="0"/>
<line stroke-width="7" id="svg_9" x1="0" x2="20" y1="50" y2="50" stroke="currentColor" fill="none" fill-opacity="0"/>
<line stroke-width="7" id="svg_11" x1="80" x2="100" y1="50" y2="50" stroke="currentColor" fill="none" fill-opacity="0"/>
</svg>`
if (game.user.isGM) {
const listElement = html.find('li')
for (const element of listElement) {
$(element).append(
`<button type='button' title='${tooltip}' class='challenge-tracker-player-list-button flex0'>${svg}</button>`
)
}
} else {
const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`)
loggedInUserListItem.append(
`<button type='button' title='${tooltip}' class='challenge-tracker-player-list-button flex0'>${svg}</button>`
)
}

// Add click event to button
html.on('click', '.challenge-tracker-player-list-button', (event) => {
ChallengeTrackerForm.open(event)
})
})

/* Draw the challenge trackers once rendered */
Hooks.on('renderChallengeTracker', async () => {
if (!game.challengeTracker) return
for (const challengeTracker of game.challengeTracker) {
if (challengeTracker._state === 1) {
challengeTracker.draw()
challengeTracker._draw()
challengeTracker.activateListenersPostDraw()
}
}
Expand Down
70 changes: 70 additions & 0 deletions scripts/flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ChallengeTrackerSettings, ChallengeTracker } from './main.js'
import { ChallengeTrackerForm } from './form.js'

export class ChallengeTrackerFlag {
/**
* Get list of flags by user
* @param {string} userId User that created the flags
**/
static getList (userId) {
const challengeTrackerList = []
if (!game.users.get(userId)?.data.flags['challenge-tracker']) return
const flagKeys = Object.keys(game.users.get(userId)?.data.flags['challenge-tracker'])
for (const flagKey of flagKeys) {
challengeTrackerList.push(game.users.get(userId)?.getFlag(ChallengeTrackerSettings.id, flagKey))
}
return challengeTrackerList
}

/**
* Get flag by owner and Challenge Tracker
* @param {string} ownerId User that owns the flag
* @param {string} challengeTrackerId Unique identifier for the Challenge Tracker
**/
static get (ownerId, challengeTrackerId) {
if (!game.users.get(ownerId)?.data.flags['challenge-tracker']) return
const flagKey = Object.keys(game.users.get(ownerId)?.data.flags['challenge-tracker']).find(ct => ct === challengeTrackerId)
if (!flagKey) return
const challengeTracker = game.users.get(ownerId)?.getFlag(ChallengeTrackerSettings.id, flagKey)
return challengeTracker
}

/**
* Set flag by owner and Challenge Tracker. Used to create a challenge tracker.
* @param {string} ownerId User that owns the flag
* @param {array} challengeTrackerOptions Challenge Tracker Options
* @param {string} challengeTrackerOptions.frameColor Hex color of the frame
* @param {string} challengeTrackerOptions.id Unique identifier of the challenge tracker
* @param {string} challengeTrackerOptions.innerColor Hex color of the inner circle
* @param {number} challengeTrackerOptions.innerCurrent Number of filled segments of the inner circle
* @param {number} challengeTrackerOptions.innerTotal Number of segments for the inner circle
* @param {string} challengeTrackerOptions.outerColor Hex color of the outer ring
* @param {number} challengeTrackerOptions.outerCurrent Number of filled segments of the outer ring
* @param {number} challengeTrackerOptions.outerTotal Number of segments for the outer ring
* @param {boolean} challengeTrackerOptions.persist true = Persist, false = Do not persist
* @param {boolean} challengeTrackerOptions.show true = Show, false = Hide
* @param {number} challengeTrackerOptions.size Size of the challenge tracker in pixels
* @param {string} challengeTrackerOptions.title Title of the challenge tracker
* @param {boolean} challengeTrackerOptions.windowed true = Windowed, false = Windowless
**/
static async set (ownerId, challengeTrackerOptions) {
await game.users.get(ownerId)?.setFlag(ChallengeTrackerSettings.id, challengeTrackerOptions.id, challengeTrackerOptions)
ChallengeTrackerForm.challengeTrackerForm?.render(false, { width: 'auto', height: 'auto' })
}

/**
* Unset flag by owner and Challenge Tracker. Used to delete a challenge tracker.
* @param {string} ownerId User that owns the flag
* @param {string} challengeTrackerId Unique identifier for the Challenge Tracker
**/
static async unset (ownerId, challengeTrackerId) {
const flagKey = Object.keys(game.users.get(ownerId)?.data.flags['challenge-tracker']).find(ct => ct === challengeTrackerId)
if (!flagKey) {
ui.notifications.error(`Challenge Tracker '${challengeTrackerId}' does not exist.`)
return
}
const deletedFlag = game.users.get(ownerId)?.unsetFlag(ChallengeTrackerSettings.id, challengeTrackerId)
ui.notifications.info(`Challenge Tracker '${challengeTrackerId}' deleted.`)
return deletedFlag
}
}
Loading

0 comments on commit b7a84d2

Please sign in to comment.