From c0f3fcbf5528536803246a364a09ddda1086f57d Mon Sep 17 00:00:00 2001
From: keithcurtis1 Filter: Click the
π
- button to filter items by type.
+ button to filter items by type. The filter button supersedes the Star Filter button
+
+ Star system: Use stars to link specific characters to a backdrop image. For instance, if a scene has several shops, you can star each proprietor for their shop image. When that image is the backdrop, the linked charactersβ tokens are highlighted in gold in the token list. This feature is disabled in Grid mode.
+ The Star Filter button in the header will filter to show only starred items. To temporarily show all characters without turning off the star filter, use the filter button to show all Characters.
@@ -1304,15 +1352,17 @@ const renderHelpHtml = (css) => `
Grid populates the tabletop with:
Only works if the current page name contains: scene, stage, theater, theatre
@@ -1363,12 +1413,151 @@ const getJukeboxPlusHandoutLink = () => { }; + +// Create or refresh GM-layer rectangle highlights for starred tokens for the given scene/page. +// - sceneName: name of the scene in state +// - pageId: the page to inspect and on which to create gmlayer highlights +const highlightStarredTokens = (sceneName, pageId) => { + if (!sceneName || !pageId) return; + const st = getState(); + st.starHighlights = st.starHighlights || {}; + + // Find scene object + let scene = null; + for (const act of Object.values(st.acts || {})) { + if (act.scenes?.[sceneName]) { + scene = act.scenes[sceneName]; + break; + } + } + if (!scene) { + st.starHighlights[pageId] = st.starHighlights[pageId] || []; + updateState(st); + return; + } + + // Remove prior highlights + const oldHighlights = st.starHighlights[pageId] || []; + oldHighlights.forEach(id => { + const p = getObj('pathv2', id); + if (p) p.remove(); + }); + st.starHighlights[pageId] = []; + + // Only starred for current backdrop + const backdropId = scene.backdropId; + if (!backdropId) { + updateState(st); + return; + } + const starredList = scene.starredAssets?.[backdropId] || []; + if (!Array.isArray(starredList) || !starredList.length) { + updateState(st); + return; + } + + const pageGraphics = findObjs({ _type: 'graphic', _pageid: pageId }); + const newHighlightIds = []; + + const padding = 12; + const strokeColor = 'gold'; + const fillColor = 'transparent'; + const strokeWidth = 4; + + const findButtonById = id => (st.items?.buttons || []).find(b => b.id === id); + + starredList.forEach(btnId => { + const btn = findButtonById(btnId); + if (!btn) return; + + let matched = []; + + if (btn.type === 'character' && btn.refId) { + const charObj = getObj('character', btn.refId); + if (charObj) { + const charName = (charObj.get('name') || '').toLowerCase(); + matched = pageGraphics.filter(g => { + try { + if (g.get('layer') !== 'objects') return false; + const repId = g.get('represents'); + if (!repId) return false; + const repChar = getObj('character', repId); + if (!repChar) return false; + return (repChar.get('name') || '').toLowerCase() === charName; + } catch { + return false; + } + }); + } + } + + if (btn.type === 'variant') { + const btnNameLower = (btn.name || '').toLowerCase(); + matched = pageGraphics.filter(g => { + try { + return ( + g.get('layer') === 'objects' && + (g.get('name') || '').toLowerCase() === btnNameLower + ); + } catch { + return false; + } + }); + } + + if (!matched.length) return; + + matched.forEach(g => { + try { + const gw = (g.get('width') || 70) + padding; + const gh = (g.get('height') || 70) + padding; + const gx = g.get('left'); + const gy = g.get('top'); + + const path = createObj('pathv2', { + _pageid: pageId, + layer: 'gmlayer', + stroke: strokeColor, + stroke_width: strokeWidth, + fill: fillColor, + shape: 'rec', + points: JSON.stringify([[0,0],[gw,gh]]), + x: gx, + y: gy, + rotation: 0 + }); + + if (path) newHighlightIds.push(path.id); + } catch (e) { + log(`[Director] highlightStarredTokens: failed to create highlight for btn ${btnId}: ${e.message}`); + } + }); + }); + + st.starHighlights[pageId] = newHighlightIds; + updateState(st); +}; + + + + + + const renderFilterBarInline = (css) => { const st = getState(); const activeFilter = st.items?.filter || 'all'; + const starMode = st.items?.starMode || false; const mode = st.settings?.mode || 'light'; const borderColor = mode === 'dark' ? '#eee' : '#444'; + // Determine if grid mode is active by checking for DL paths on GM page + const pid = Campaign().get('playerpageid'); + let gridModeActive = false; + if (pid) { + const existingPaths = findObjs({ _type: 'pathv2', _pageid: pid, layer: 'walls' }); + gridModeActive = existingPaths.some(p => p.get('stroke') === '#84d162'); + } + // Build dynamic option strings const characters = findObjs({ _type: 'character' }).sort((a, b) => a.get('name').localeCompare(b.get('name'))); const handouts = findObjs({ _type: 'handout' }).sort((a, b) => a.get('name').localeCompare(b.get('name'))); @@ -1378,8 +1567,6 @@ const renderFilterBarInline = (css) => { const buildOpts = (objs, labelFn = o => o.get('name')) => objs.map(o => `${labelFn(o).replace(/"/g, """)},${o.id}`).join('|'); - // Suggested replacement for following line to catch names with double quotes. - //objs.map(o => `${labelFn(o)},${o.id}`).join('|'); const charOpts = buildOpts(characters); const handoutOpts = buildOpts(handouts); @@ -1387,6 +1574,8 @@ const renderFilterBarInline = (css) => { const tableOpts = buildOpts(tables); const trackOpts = buildOpts(tracks, t => t.get('title')); + const starFilterBtn = `β `; + const filterByType = (st.items.filter === "all" ? '': '= ' + st.items.filter + 's'); const buttons = [ `H`, `C`, @@ -1395,7 +1584,10 @@ const renderFilterBarInline = (css) => { `M`, `R`, `π` + style="${css.itemAddBadge};" title="Filter Items by Type">π + ${filterByType}`, + // Only show starFilterBtn if NOT in grid mode + ...(!gridModeActive ? [starFilterBtn] : []) ]; return buttons.join(''); @@ -1409,16 +1601,57 @@ const renderItemsList = (css) => { const isEditMode = !!st.items?.editMode; const currentScene = st.activeScene; const activeFilter = st.items?.filter || 'all'; + const starMode = st.items?.starMode || false; + + // --- Detect grid mode by checking if any DL paths with stroke #84d162 exist on current player's page --- + const pid = Campaign().get('playerpageid'); + let gridModeActive = false; + if (pid) { + const existingPaths = findObjs({ _type: 'pathv2', _pageid: pid, layer: 'walls' }); + gridModeActive = existingPaths.some(p => p.get('stroke') === '#84d162'); + } + + const stData = st; // reuse state reference + + // Find sceneObj and backdropId once for reuse + let sceneObj = null; + let backdropId = null; + if (currentScene) { + for (const act of Object.values(stData.acts || {})) { + if (act.scenes?.[currentScene]) { + sceneObj = act.scenes[currentScene]; + break; + } + } + backdropId = sceneObj?.backdropId; + } + + // Build set of starred assets for current scene if starMode is active + let starredAssetsSet = new Set(); + if (currentScene && starMode && sceneObj && backdropId && sceneObj.starredAssets?.[backdropId]) { + starredAssetsSet = new Set(sceneObj.starredAssets[backdropId]); + } + + // Filter items by scene, type, exclude 'action', and if starMode is active, filter to starred only const items = (st.items?.buttons || []).filter(btn => { const sceneMatch = btn.scene === currentScene; - const typeMatch = activeFilter === 'all' || - btn.type === activeFilter || + const typeMatch = activeFilter === 'all' || + btn.type === activeFilter || (activeFilter === 'character' && btn.type === 'variant'); const excludeActions = btn.type !== 'action'; - return sceneMatch && typeMatch && excludeActions; + + if (!sceneMatch || !typeMatch || !excludeActions) return false; + + // Apply star filter ONLY when activeFilter is 'all' + if (activeFilter === 'all' && starMode) { + return starredAssetsSet.has(btn.id); + } + + return true; }); + // Fetch lookup objects for handouts, characters, macros, tables const handouts = findObjs({ _type: 'handout' }).sort((a, b) => a.get('name').localeCompare(b.get('name'))); const characters = findObjs({ _type: 'character' }).sort((a, b) => a.get('name').localeCompare(b.get('name'))); const macros = findObjs({ _type: 'macro' }).sort((a, b) => a.get('name').localeCompare(b.get('name'))); @@ -1429,6 +1662,7 @@ const renderItemsList = (css) => { let labelText = btn.name; let tooltipAttr = ''; + // === existing type-based logic unchanged === if (btn.type === 'action') { if (!btn.refId) { const options = characters.map(c => `${c.get('name')},${c.id}`).join('|'); @@ -1526,6 +1760,7 @@ const renderItemsList = (css) => { tooltipAttr = ` title="Item placeholder"`; } + // --- build edit controls --- const editControls = isEditMode ? ` ${Pictos('p')} @@ -1533,11 +1768,26 @@ const renderItemsList = (css) => { ` : ''; + // --- compute star HTML, omit if grid mode active or in edit mode --- + let starHTML = ''; + if (!gridModeActive && !isEditMode) { + if (!backdropId) { + starHTML = `β `; + } else { + const starredList = sceneObj.starredAssets?.[backdropId] || []; + const isStarred = Array.isArray(starredList) && starredList.includes(btn.id); + starHTML = `β `; + } + } + + // --- return the row: container is relative so star positions itself INSIDE the button area --- return ` -The Director script supports "theater of the mind" style play in Roll20. It provides an interface for managing scenes, associated images, audio, and relevant game assets β all organized within a persistent handout.
+Watch a video demo of Director.
+The interface appears in a Roll20 handout. It consists of four main sections:
+Acts group together related scenes. Use the + + Add Act + button to create an act.
+In Edit Mode, act-level options include: +
Each scene represents a distinct time and place. Click a scene name to set it active. The active scene determines what appears in the Images and Items sections.
+In Edit Mode, scene controls include: +
Backdrop is the main background image for the scene, displayed on the map layer to set the overall environment.
+Highlights are supplementary images layered above the backdrop on the object layer, used to draw attention to specific elements or areas.
+When a scene is set, the backdrop is placed on the map layer, while all highlights appear on the object layer, aligned left beyond the page boundary for easy visibility and interaction.
+To use a highlight, the gm can drag it onto the page, or select it and use the shift-Z keyboard command to preview it to the players.
+Highlights and Bacdrops can be switched on the fly by using the buttons found on each image in the handout (see below) +
To add an image: +
Click to toggle. When this button is red, the audio track auto-play behavior of backdrops is suppressed.
+Items define what is placed or triggered when a scene is set. Items are scoped per scene.
+ +Click a badge to add a new item:
+Variants are token snapshots that share a sheet. Use these when default tokens cannot be reliably spawned, or to represent unique versions of a shared character sheet.
+ +In Edit Mode, each item shows:
+Filter: Click the + π + button to filter items by type. The filter button supersedes the Star Filter button +
+
+ Star system: Use stars to link specific characters to a backdrop image. For instance, if a scene has several shops, you can star each proprietor for their shop image. When that image is the backdrop, the linked charactersβ tokens are highlighted in gold in the token list. This feature is disabled in Grid mode.
+ The Star Filter button in the header will filter to show only starred items. To temporarily show all characters without turning off the star filter, use the filter button to show all Characters.
+
Scene populates the tabletop with: +
Grid populates the tabletop with: +
Only works if the current page name contains: scene, stage, theater, theatre
+ +Wipe the Scene removes all placed images and stops all audio.
+Only functions on valid stage pages.
+ +${Pictos(')')} toggles editing. When enabled:
+If you have the Jukebox Plus script installed, this button will display and will put a link in chat for opening that program's controls.
+Displays this Help documentation. While in help mode, this changes to read "Exit Help".
+This button appears only while in Help mode. Pressing it will create a handout containing the help documentaiton. Useful if you want to see the documentation and the interface at the same time.
+ + +The interface is primary, but the following macros can be used in chat or action buttons:
++!director --set-scene +!director --wipe-scene +!director --new-act|Act I +!director --new-scene|Act I|Opening Scene +!director --capture-image ++
+ Director + Exit Help + Make Help Handout + | +
+ Director + + + + Wipe the Scene + + + Stop Audio + +${getJukeboxPlusHandoutLink()} + + + ${st.helpMode ? 'Exit Help' : 'Help'} + + + + ${Pictos(st.items?.editMode ? ')' : '(')} + | +||
+
+ Acts
+ + Add Act
+
+ ${actsHtml}
+
+ + +
+
+ Settings ${st.settings.settingsExpanded ? 'β΄' : 'βΎ'}
+
+ ${st.settings.settingsExpanded ? `
+
+
+ β» Repair
+ ` : ''}
+
+ |
+
+
+ Images
+ + Add Image
+
+ ${Pictos('m')}
+
+
+
+
+ ${imagesHTML}
+ |
+
+
+ Items ${renderFilterBarInline(css)}
+
+
+${renderItemsList(css)}
+
+ |
+