Skip to content

Commit b7a84d2

Browse files
committed
Persist Challenge Trackers
- Persist challenge trackers across sessions using user flags. - Add forms to manage challenge trackers. - Add button on player list to open forms.
1 parent bbff71a commit b7a84d2

14 files changed

+819
-199
lines changed

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Challenge Tracker
2-
An interactive aid to track successes and failures in challenges.
2+
An interactive aid to track successes and failures in challenges à la D&D 4e-inspired skill challenges and Blades in the Dark progress clocks.
33

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

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

1212
## How to Use
13+
### Using the Player List
14+
![challenge-tracker-macro](./images/challenge-tracker-player-list.png)
15+
1. Click the button ![challenge-tracker-macro](./images/challenge-tracker-player-list-button.png) in the player list.
16+
17+
![challenge-tracker-macro](./images/challenge-tracker-list.png)
18+
19+
2. Click 'Create New' to create a new Challenge Tracker.
20+
3. Fill in the options and click 'Save and Close'.
21+
- Click 'Open' to open a Challenge Tracker.
22+
- Click 'Edit' to edit an existing Challenge Tracker.
23+
- Click 'Delete' to delete an existing Challenge Tracker.
24+
25+
### Using Macros
1326
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).
1427
2. Execute the macro to open the Challenge Tracker.
1528

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

1831
## Advanced Options
1932
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`:
20-
- **show:** Set to `true` to show the Challenge Tracker to your players. Default is `false`. Example: `show: true`
21-
- **outerCurrent:** Set the number of completed segments on the outer ring (successes). Default is `0`. Example: `outerCurrent: 3`
22-
- **innerCurrent:** Set the number of completed segments on the inner circle (failures). Default is `0`. Example: `innerCurrent: 3`
33+
- **outerCurrent:** Set the number of completed segments on the outer ring (successes). Default is `0`. Example: `outerCurrent: 3
34+
- **innerCurrent:** Set the number of completed segments on the inner circle (failures). Default is `0`. Example: `innerCurrent:
2335
- **outerColor:** Set the hex color of the outer ring (successes). The 'Outer Color' module setting will be ignored. Example: `outerColor: '#0000FF'`
2436
- **innerColor:** Set the hex color of the inner circle (failures). The 'Inner Color' module setting will be ignored. Example: `innerColor: '#0000FF'`
2537
- **frameColor:** Set the hex color of the frame. The 'Frame Color' module setting will be ignored. Example: `frameColor: '#0000FF'`
38+
- **persist:** Set to `true` to persist the Challenge Tracker across sessions. Default is `false`. Example: `persist: true`
39+
- **show:** Set to `true` to show the Challenge Tracker to your players. Default is `false`. Example: `show: true`
2640
- **size:** Set the size of the Challenge Tracker in pixels between 200 to 600. The 'Size' module setting will be ignored. Example: `size: 400`
27-
- **windowed:** Set the Challenge Tracker to windowed (true) or windowless (false). The 'Windowed' module setting will be ignored. Example: `windowed: false`
2841
- **title:** Set the title of the Challenge Tracker in the window header. Default is `Challenge Tracker`. Example: `title: 'Skill Challenge 1'`
42+
- **windowed:** Set the Challenge Tracker to windowed (true) or windowless (false). The 'Windowed' module setting will be ignored. Example: `windowed: false`
2943

3044
## Examples
3145
### Progress Clock

images/challenge-tracker-list.png

118 KB
Loading
1.02 KB
Loading
21.6 KB
Loading

languages/en.json

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
11
{
2-
"settings": {
3-
"allowShow" : {
4-
"name": "Show to Others",
5-
"hint": "Allow users with this role (and above) to show Challenge Trackers to others (Requires reload)"
6-
},
7-
"outerColor" : {
8-
"name": "Outer Color",
9-
"hint": "Set the default color for the outer ring",
10-
"label": "Color Picker"
11-
},
12-
"innerColor" : {
13-
"name": "Inner Color",
14-
"hint": "Set the default color for the inner circle",
15-
"label": "Color Picker"
16-
},
17-
"frameColor" : {
18-
"name": "Frame Color",
19-
"hint": "Set the default color of the frame",
20-
"label": "Color Picker"
21-
},
22-
"size" : {
23-
"name": "Size",
24-
"hint": "Set the default size of the challenge tracker in pixels"
25-
},
26-
"windowed" : {
27-
"name": "Windowed",
28-
"hint": "Set the challenge tracker to windowed by default"
29-
},
30-
"scroll" : {
31-
"name": "Scroll",
32-
"hint": "Enable the scroll wheel for increasing/decreasing segments"
33-
}
34-
}
2+
"challengeTracker": {
3+
"labels": {
4+
"challengeTrackerTitle": "Challenge Tracker",
5+
"challengeTrackerButtonTitle": "Challenge Tracker"
6+
},
7+
"settings": {
8+
"allowShow" : {
9+
"name": "Show to Others",
10+
"hint": "Allow users with this role (and above) to show Challenge Trackers to others (Requires reload)"
11+
},
12+
"outerColor" : {
13+
"name": "Outer Color",
14+
"hint": "Set the default color for the outer ring",
15+
"label": "Color Picker"
16+
},
17+
"innerColor" : {
18+
"name": "Inner Color",
19+
"hint": "Set the default color for the inner circle",
20+
"label": "Color Picker"
21+
},
22+
"frameColor" : {
23+
"name": "Frame Color",
24+
"hint": "Set the default color of the frame",
25+
"label": "Color Picker"
26+
},
27+
"size" : {
28+
"name": "Size",
29+
"hint": "Set the default size of the challenge tracker in pixels"
30+
},
31+
"windowed" : {
32+
"name": "Windowed",
33+
"hint": "Set the challenge tracker to windowed by default"
34+
},
35+
"scroll" : {
36+
"name": "Scroll",
37+
"hint": "Enable the scroll wheel for increasing/decreasing segments"
38+
}
39+
}
40+
}
3541
}

module.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "challenge-tracker",
33
"title": "Challenge Tracker",
4-
"description": "An interactive aid to track successes and failures in challenges",
5-
"version": "0.6",
4+
"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",
5+
"version": "0.7",
66
"library": "false",
77
"manifestPlusVersion": "1.0.0",
88
"minimumCoreVersion": "9",

scripts/challenge-tracker.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { ChallengeTracker } from './main.js'
22
import { Utils } from './utils.js'
33
import { Settings } from './settings.js'
4+
import { ChallengeTrackerForm, ChallengeTrackerEditForm } from './form.js'
5+
import { ChallengeTrackerFlag } from './flags.js'
46

57
Hooks.once('init', () => {
68
Settings.init()
9+
ChallengeTrackerForm.init()
10+
ChallengeTrackerEditForm.init()
11+
Handlebars.registerHelper('ifEquals', function (arg1, arg2, options) {
12+
return (arg1 === arg2) ? options.fn(this) : options.inverse(this)
13+
})
714
})
815

916
Hooks.once('colorSettingsInitialized', async () => {
@@ -38,11 +45,46 @@ Hooks.once('ready', async () => {
3845
}
3946
})
4047

48+
/* Add buttons to the Player List */
49+
Hooks.on('renderPlayerList', (playerList, html) => {
50+
const tooltip = game.i18n.localize('challengeTracker.labels.challengeTrackerButtonTitle')
51+
const svg = `<svg width="100" height="100" viewBox="-5 -5 110 110" xmlns="http://www.w3.org/2000/svg">
52+
<title>challenge-tracker-button-icon</title>
53+
<ellipse stroke-width="7" id="outer_circle" cx="50" cy="50" rx="50" ry="50" stroke="currentColor" fill="none" fill-opacity="0"/>
54+
<ellipse stroke-width="7" id="inner_circle" cx="50" cy="50" rx="30" ry="30" stroke="currentColor" fill="none" fill-opacity="0"/>
55+
<line stroke-width="7" id="svg_4" x1="50" x2="50" y1="0" y2="50" stroke="currentColor" fill="none" fill-opacity="0"/>
56+
<line stroke-width="7" id="svg_5" x1="50" x2="76" y1="50" y2="65" stroke="currentColor" fill="none" fill-opacity="0"/>
57+
<line stroke-width="7" id="svg_6" x1="50" x2="24" y1="50" y2="65" stroke="currentColor" fill="none" fill-opacity="0"/>
58+
<line stroke-width="7" id="svg_8" x1="50" x2="50" y1="80" y2="100" stroke="currentColor" fill="none" fill-opacity="0"/>
59+
<line stroke-width="7" id="svg_9" x1="0" x2="20" y1="50" y2="50" stroke="currentColor" fill="none" fill-opacity="0"/>
60+
<line stroke-width="7" id="svg_11" x1="80" x2="100" y1="50" y2="50" stroke="currentColor" fill="none" fill-opacity="0"/>
61+
</svg>`
62+
if (game.user.isGM) {
63+
const listElement = html.find('li')
64+
for (const element of listElement) {
65+
$(element).append(
66+
`<button type='button' title='${tooltip}' class='challenge-tracker-player-list-button flex0'>${svg}</button>`
67+
)
68+
}
69+
} else {
70+
const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`)
71+
loggedInUserListItem.append(
72+
`<button type='button' title='${tooltip}' class='challenge-tracker-player-list-button flex0'>${svg}</button>`
73+
)
74+
}
75+
76+
// Add click event to button
77+
html.on('click', '.challenge-tracker-player-list-button', (event) => {
78+
ChallengeTrackerForm.open(event)
79+
})
80+
})
81+
82+
/* Draw the challenge trackers once rendered */
4183
Hooks.on('renderChallengeTracker', async () => {
4284
if (!game.challengeTracker) return
4385
for (const challengeTracker of game.challengeTracker) {
4486
if (challengeTracker._state === 1) {
45-
challengeTracker.draw()
87+
challengeTracker._draw()
4688
challengeTracker.activateListenersPostDraw()
4789
}
4890
}

scripts/flags.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ChallengeTrackerSettings, ChallengeTracker } from './main.js'
2+
import { ChallengeTrackerForm } from './form.js'
3+
4+
export class ChallengeTrackerFlag {
5+
/**
6+
* Get list of flags by user
7+
* @param {string} userId User that created the flags
8+
**/
9+
static getList (userId) {
10+
const challengeTrackerList = []
11+
if (!game.users.get(userId)?.data.flags['challenge-tracker']) return
12+
const flagKeys = Object.keys(game.users.get(userId)?.data.flags['challenge-tracker'])
13+
for (const flagKey of flagKeys) {
14+
challengeTrackerList.push(game.users.get(userId)?.getFlag(ChallengeTrackerSettings.id, flagKey))
15+
}
16+
return challengeTrackerList
17+
}
18+
19+
/**
20+
* Get flag by owner and Challenge Tracker
21+
* @param {string} ownerId User that owns the flag
22+
* @param {string} challengeTrackerId Unique identifier for the Challenge Tracker
23+
**/
24+
static get (ownerId, challengeTrackerId) {
25+
if (!game.users.get(ownerId)?.data.flags['challenge-tracker']) return
26+
const flagKey = Object.keys(game.users.get(ownerId)?.data.flags['challenge-tracker']).find(ct => ct === challengeTrackerId)
27+
if (!flagKey) return
28+
const challengeTracker = game.users.get(ownerId)?.getFlag(ChallengeTrackerSettings.id, flagKey)
29+
return challengeTracker
30+
}
31+
32+
/**
33+
* Set flag by owner and Challenge Tracker. Used to create a challenge tracker.
34+
* @param {string} ownerId User that owns the flag
35+
* @param {array} challengeTrackerOptions Challenge Tracker Options
36+
* @param {string} challengeTrackerOptions.frameColor Hex color of the frame
37+
* @param {string} challengeTrackerOptions.id Unique identifier of the challenge tracker
38+
* @param {string} challengeTrackerOptions.innerColor Hex color of the inner circle
39+
* @param {number} challengeTrackerOptions.innerCurrent Number of filled segments of the inner circle
40+
* @param {number} challengeTrackerOptions.innerTotal Number of segments for the inner circle
41+
* @param {string} challengeTrackerOptions.outerColor Hex color of the outer ring
42+
* @param {number} challengeTrackerOptions.outerCurrent Number of filled segments of the outer ring
43+
* @param {number} challengeTrackerOptions.outerTotal Number of segments for the outer ring
44+
* @param {boolean} challengeTrackerOptions.persist true = Persist, false = Do not persist
45+
* @param {boolean} challengeTrackerOptions.show true = Show, false = Hide
46+
* @param {number} challengeTrackerOptions.size Size of the challenge tracker in pixels
47+
* @param {string} challengeTrackerOptions.title Title of the challenge tracker
48+
* @param {boolean} challengeTrackerOptions.windowed true = Windowed, false = Windowless
49+
**/
50+
static async set (ownerId, challengeTrackerOptions) {
51+
await game.users.get(ownerId)?.setFlag(ChallengeTrackerSettings.id, challengeTrackerOptions.id, challengeTrackerOptions)
52+
ChallengeTrackerForm.challengeTrackerForm?.render(false, { width: 'auto', height: 'auto' })
53+
}
54+
55+
/**
56+
* Unset flag by owner and Challenge Tracker. Used to delete a challenge tracker.
57+
* @param {string} ownerId User that owns the flag
58+
* @param {string} challengeTrackerId Unique identifier for the Challenge Tracker
59+
**/
60+
static async unset (ownerId, challengeTrackerId) {
61+
const flagKey = Object.keys(game.users.get(ownerId)?.data.flags['challenge-tracker']).find(ct => ct === challengeTrackerId)
62+
if (!flagKey) {
63+
ui.notifications.error(`Challenge Tracker '${challengeTrackerId}' does not exist.`)
64+
return
65+
}
66+
const deletedFlag = game.users.get(ownerId)?.unsetFlag(ChallengeTrackerSettings.id, challengeTrackerId)
67+
ui.notifications.info(`Challenge Tracker '${challengeTrackerId}' deleted.`)
68+
return deletedFlag
69+
}
70+
}

0 commit comments

Comments
 (0)