├── .gitignore ├── plugin_diceroller.sfile.md ├── mythicgme_ui.sfile.md ├── system.sfile.md ├── rpgtools.sfile.md ├── files.sfile.md ├── clips_ui.sfile.md ├── clips.sfile.md ├── mythicv2_ui.sfile.md ├── arrows.sfile.md ├── notepick.sfile.md ├── lipsum.sfile.md ├── une_ui.sfile.md ├── state.sfile.md ├── notevars.sfile.md ├── une.sfile.md ├── cards_pileviewer.sfile.md ├── lists_ui.sfile.md ├── mythicgme.sfile.md ├── lists.sfile.md ├── cards_ui.sfile.md └── mythicv2.sfile.md /.gitignore: -------------------------------------------------------------------------------- 1 | Ξ_state.data.md 2 | Ξ_libraryVersion.md 3 | -------------------------------------------------------------------------------- /plugin_diceroller.sfile.md: -------------------------------------------------------------------------------- 1 | A shortcut to run commands through the Dice Roller plugin. 2 | 3 | __ 4 | ``` 5 | ^diceroller (.+)$ 6 | ``` 7 | __ 8 | ```js 9 | // Get the diceroller plugin (or exit if not available) 10 | const diceRollerPlugin = app.plugins.getPlugin("obsidian-dice-roller"); 11 | if (!diceRollerPlugin) 12 | { 13 | return expText("Command not run. Dice Roller plugin not available."); 14 | } 15 | 16 | // Roll the command parameter with the plugin 17 | const diceRoller = await diceRollerPlugin.getRoller($1); 18 | return await diceRoller.roll(); 19 | ``` 20 | __ 21 | diceroller {command: text} - Runs {command} through the Dice Roller plugin and expands to the result. 22 | -------------------------------------------------------------------------------- /mythicgme_ui.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | An extension to __mythicgme.sfile__ that provides graphical ui versions of shortcuts. 6 | 7 | 8 | __ 9 | ``` 10 | ^sfile setup$ 11 | ``` 12 | __ 13 | ```js 14 | // Block loading if mythicv2 is already setup (incompatible shortcut-files) 15 | if (_inlineScripts.inlineScripts.sfileIndices["mythicv2"]) 16 | { 17 | return true; 18 | } 19 | ``` 20 | __ 21 | Sets up this shortcut-file 22 | 23 | 24 | __ 25 | ``` 26 | ^ui fate$ 27 | ``` 28 | __ 29 | ```js 30 | // Data 31 | const odds = 32 | [ 33 | "Impossible", "No way", "Very unlikely", 34 | "Unlikely", "50 / 50", "Somewhat likely", 35 | "Likely", "Very likely", "Near sure thing", 36 | "Sure thing", "Has to be" 37 | ]; 38 | 39 | // Choose the odds to roll fate with 40 | const pick = popups.pick("Choose the odds", odds, 4, 11); 41 | if (pick === null) { return null; } 42 | 43 | // Roll fate with the chosen odds 44 | return expand("fate " + (pick - 4)); 45 | ``` 46 | __ 47 | ui fate - Asks the user to choose the odds for this fate check. 48 | Displays the answer to a yes/no question, possibly with a random event attached. 49 | 50 | 51 | __ 52 | ``` 53 | ^ui scene$ 54 | ``` 55 | __ 56 | ```js 57 | // Choose chaos adjustment for the scene advancement 58 | const pick = 59 | popups.pick("How was the previous scene?", [ "More controlled", "More chaotic" ], 60 | 1, 2); 61 | if (pick === null) { return null; } 62 | 63 | // Run the scene with the chosen chaos adjustments 64 | return expand("scene " + (pick * 2 - 1)); 65 | ``` 66 | __ 67 | ui scene - Asks the user to choose whether chaos increased or decreased last scene. 68 | Starts a new scene with the chosen chaos value adjustment. 69 | -------------------------------------------------------------------------------- /system.sfile.md: -------------------------------------------------------------------------------- 1 | Shortcuts to work with the system: Obsidian, JS, InlineScripts backend, etc. 2 | 3 | 4 | __ 5 | ``` 6 | ^sfile setup$ 7 | ``` 8 | __ 9 | ```js 10 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 11 | 12 | // Setup session state 13 | confirmObjectPath("_inlineScripts.system.lastError", "NONE"); 14 | 15 | // Event callback - inlineScripts.onError 16 | confirmObjectPath( 17 | "_inlineScripts.inlineScripts.listeners.onError.system", 18 | (errorMessage) => 19 | { 20 | _inlineScripts.system.lastError = errorMessage; 21 | }); 22 | ``` 23 | __ 24 | Sets up this shortcut-file 25 | 26 | 27 | __ 28 | ``` 29 | ^sfile shutdown$ 30 | ``` 31 | __ 32 | ```js 33 | // Event callback removal 34 | delete _inlineScripts.inlineScripts?.listeners?.onError?.system; 35 | 36 | // Session state removal 37 | delete _inlineScripts.system; 38 | ``` 39 | __ 40 | Shuts down this shortcut-file 41 | 42 | 43 | __ 44 | ``` 45 | ^sys lasterror$ 46 | ``` 47 | __ 48 | ```js 49 | return expFormat( 50 | "Last error:\n> " + _inlineScripts.system.lastError.replaceAll("\n", "\n> ")); 51 | ``` 52 | __ 53 | sys lasterror - Expands to the last expansion error that was posted to the console. This does not include errors that were created by a shortcut, only those created by __Inline Scripts__ itself, i.e. those the trigger the message: `ERROR: Shortcut expansion issues. (see console for details)`. 54 | 55 | 56 | __ 57 | ``` 58 | ^sys runjs (.*)$ 59 | ``` 60 | __ 61 | ```js 62 | return eval($1); 63 | ``` 64 | __ 65 | sys runjs {code: text} - Run the javascript in {code}. Can run multiple statements separated with newline characters (`\\n`). 66 | 67 | 68 | __ 69 | ``` 70 | ^sys triggererror$ 71 | ``` 72 | __ 73 | ```js 74 | bad code here! 75 | ``` 76 | __ 77 | sys triggererror - Create an expansion error. Useful for testing the `sys lasterror` shortcut. 78 | -------------------------------------------------------------------------------- /rpgtools.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts to help in playing tabletop rpgs, either group or solo. 6 | 7 | 8 | __ 9 | __ 10 | ```js 11 | // Make a roll from 1 to max. 12 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 13 | 14 | // Pick an item from array a. 15 | function aPick(a) { return a[roll(a.length)-1]; } 16 | 17 | // Pick an item from array a, weighted by element wIndex of the item. If theRoll is 18 | // passed, use that as the roll. 19 | function aPickWeight(a, wIndex, theRoll) 20 | { 21 | wIndex = wIndex || 1; 22 | theRoll = theRoll || roll(a.last()[wIndex]); 23 | for (let i = 0; i < a.length; i++) 24 | { 25 | if (a[i][wIndex] >= theRoll) 26 | { 27 | return a[i]; 28 | } 29 | } 30 | return a.last(); 31 | } 32 | ``` 33 | __ 34 | Helper scripts 35 | 36 | 37 | __ 38 | ``` 39 | ^tbl twist$ 40 | ``` 41 | __ 42 | ```js 43 | // Data 44 | let target = ["ENVIRONMENT","ALLY","ENEMY","UNKNOWN","SOMETHING","INFORMATION"]; 45 | let circumstance = ["MISSING / GONE / LOST / FORGOTTEN","EXPLOSIVE / DANGEROUS / SENSITIVE","BLOCKED / HIDDEN / TRAPPED","ARRIVED / RECENT / NEW","DEAD / DESTROYED / UNRECOVERABLE","BROKEN / DAMAGED / INJURED","FLOODED / OVERFLOWED / EXCEEDED","SABOTAGED / CAPTURED / TAKEN","CORRUPTED / TWISTED / INSANE","FALSE / FAKE / LYING / TREASON","MISTAKEN / ERROR / FALSE / WRONG","LEAKED / DISCOVERED / KNOWN","ERRATIC / UNRELIABLE / FAILING / INSUFFICIENT","DISRUPTED / MODIFIED / ARTIFICIAL","SURPRISING / ALTERED / UNEXPECTED","YOUNGER / OLDER / NEWER / OBSOLETE","SICK / HAZARDOUS / POISON / TOXIC","PREPARED / ARMED / EXPECTING","UNPREPARED / UNARMED / UNEXPECTED","DELAYED / TIMED / WAIT"]; 46 | 47 | // Roll from the data and return the result. The final empty string allows 48 | // formatting to be removed by the caller. 49 | return expFormat([ "Twist:\n", aPick(target), " - ", aPick(circumstance), "" ]); 50 | 51 | ``` 52 | __ 53 | tbl twist - Rolls a plot twist from random tables. Source. 54 | -------------------------------------------------------------------------------- /files.sfile.md: -------------------------------------------------------------------------------- 1 | Shortcuts for working with files in the vault. 2 | 3 | 4 | __ 5 | ``` 6 | ^files extensionchange ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ?([^ \t\\:*?"<>|]+|)$ 7 | ``` 8 | __ 9 | ```js 10 | // File path parameter - remove quotes 11 | $1 = $1.replaceAll(/^\"|\"$/g, ""); 12 | 13 | // Extension parameter - default to "md" 14 | $2 ||= "md"; 15 | 16 | // Confirm file path parameter is valid 17 | if (!app.vault.fileMap[$1]) 18 | { 19 | return expFormat("Extension not changed. File __" + $1 + "__ not found."); 20 | } 21 | 22 | // Get the current extension position 23 | let extensionIndex = $1.lastIndexOf("."); 24 | if (extensionIndex == -1) { extensionIndex = $1.length; } 25 | 26 | // Add new extension 27 | const newPath = $1.slice(0, extensionIndex) + "." + $2; 28 | app.vault.adapter.rename($1, newPath); 29 | 30 | return expFormat("File __" + $1 + "__ is now __" + newPath + "__."); 31 | ``` 32 | __ 33 | files extensionchange {file name: path text} {extension: nospace text, default: "md"} - Changes the extension of file {file name} to {extension}. 34 | 35 | 36 | __ 37 | ``` 38 | ^files? shortcutbatch ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) (.*)$ 39 | ``` 40 | __ 41 | ```js 42 | // Remove quotes around path parameter 43 | $1 = $1.replaceAll(/^\"|\"$/g, ""); 44 | 45 | // Confirm that the path is a valid folder, early out if not 46 | const folder = app.vault.fileMap[$1]; 47 | if (!folder) 48 | { 49 | return expFormat("Extension not changed. Folder __" + $1 + "__ not found."); 50 | } 51 | if (!folder.children) 52 | { 53 | return expFormat("Extension not changed. __" + $1 + "__ is not a folder."); 54 | } 55 | 56 | // Find an unused list name (to use "lists shortcutbatch" shortcut) 57 | let tmpListName = 0; 58 | while (_inlineScripts.state.sessionState.lists["L" + tmpListName]) 59 | { 60 | tmpList++; 61 | } 62 | tmpListName = "L" + tmpListName; 63 | 64 | // Popuplate list with filenames 65 | for (const child of folder.children) 66 | { 67 | expand("lists add " + tmpListName + " " + child.path); 68 | } 69 | 70 | // Run shortcutbatch on the list 71 | let result = expand("lists shortcutbatch " + tmpList + " " + $2); 72 | 73 | // Fux the resulting expansion - "for folder" instead of "for list" 74 | result = result.replace("for list __" + tmpList, "for folder __" + $1); 75 | 76 | // Remove the list now that we're done with it 77 | expand("lists removelist " + tmpListName); 78 | 79 | // Return the result 80 | return result; 81 | ``` 82 | __ 83 | files shortcutbatch {folder name: path text} {shortcut: text} - Runs shortcut {shortcut} once for each file in folder {folder name}, replacing "%1" in {shortcut} with the file's name. 84 | -------------------------------------------------------------------------------- /clips_ui.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | An extension to __clips.sfile__ that provides graphical ui versions of shortcuts. 6 | 7 | 8 | __ 9 | ``` 10 | ^ui clips? reset$ 11 | ``` 12 | __ 13 | ```js 14 | return expand("clips reset"); 15 | ``` 16 | __ 17 | ui clips reset - Removes all clips. 18 | 19 | 20 | __ 21 | ``` 22 | ^ui clips?$ 23 | ``` 24 | __ 25 | ```js 26 | return expand("clips"); 27 | ``` 28 | __ 29 | ui clips - Lists all stored clips. 30 | 31 | 32 | __ 33 | ``` 34 | ^ui clips? set$ 35 | ``` 36 | __ 37 | ```js 38 | // Choose the clip name 39 | const clipNames = Object.keys(_inlineScripts.state.sessionState.clips).sort(); 40 | let name = popups.input("Enter a clip name to set", "", clipNames); 41 | if (!name) { return null; } 42 | name = name.replaceAll(" ", "_"); 43 | 44 | // Choose the clip value 45 | const value = popups.input("Enter a value for clip " + name + ""); 46 | if (value === null) { return null; } 47 | 48 | return expand("clips set " + name + " " + value); 49 | ``` 50 | __ 51 | ui clips set - User enters a clip name and a clip value. The value is then assigned to the clip name. 52 | 53 | 54 | __ 55 | ``` 56 | ^ui (?:clips? get|cg)$ 57 | ``` 58 | __ 59 | ```js 60 | // Choose the clip from the list of clips 61 | const clipNames = Object.keys(_inlineScripts.state.sessionState.clips).sort(); 62 | if (!clipNames.length) 63 | { 64 | return ""; 65 | } 66 | const pick = popups.pick("Choose a clip to get", clipNames, 0, "adaptive"); 67 | if (pick === null) { return null; } 68 | 69 | return expand("clips get " + clipNames[pick]); 70 | ``` 71 | __ 72 | ui clips get - User chooses a clip, which is then expanded into the note. 73 | - Alternative: __ui cg__ 74 | 75 | 76 | __ 77 | ``` 78 | ^ui clips? expansion$ 79 | ``` 80 | __ 81 | ```js 82 | // Choose the clip name 83 | const clipNames = Object.keys(_inlineScripts.state.sessionState.clips).sort(); 84 | let name = popups.input("Enter a clip name for the prior expansion", "", clipNames); 85 | if (!name) { return null; } 86 | name = name.replaceAll(" ", "_"); 87 | 88 | return expand("clips expansion " + name); 89 | ``` 90 | __ 91 | ui clips expansion - User enters a clip name. The prior expansion's text is then assigned to the clip name. 92 | 93 | 94 | __ 95 | ``` 96 | ^ui clips? remove$ 97 | ``` 98 | __ 99 | ```js 100 | // Choose the clip name 101 | const clipNames = Object.keys(_inlineScripts.state.sessionState.clips).sort(); 102 | if (!clipNames.length) 103 | { 104 | return expFormat("Clip not removed. No clips to remove."); 105 | } 106 | const pick = await popups.pick("Choose a clip to remove", clipNames, 0, "adaptive"); 107 | if (pick === null) { return null; } 108 | 109 | return expand("clips remove " + clipNames[pick]); 110 | ``` 111 | __ 112 | ui clips remove - User chooses a clip, which is then removed. 113 | -------------------------------------------------------------------------------- /clips.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts that let the user manage clips of text. A "clip" is a bit of named text that can be quickly added to your note. Similar to a shortcut, but simplistic and tied to the session state. 6 | 7 | Uses __state.sfile__ shortcut-file (optional). 8 | It uses this to save & load the clips. 9 | 10 | This shortcut-file has supplementary shortcut files: 11 | __clips_ui.sfile__ - Graphical UI versions of some of these shortcuts. 12 | 13 | 14 | __ 15 | ``` 16 | ^sfile setup$ 17 | ``` 18 | __ 19 | ```js 20 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 21 | 22 | // Make sure state and "pirorExpansion" value are setup 23 | confirmObjectPath("_inlineScripts.state.sessionState.clips"); 24 | confirmObjectPath("_inlineScripts.clips.priorExpansion", ""); 25 | 26 | // Event callback - state.onReset 27 | confirmObjectPath( 28 | "_inlineScripts.state.listeners.onReset.clips", 29 | function() 30 | { 31 | expand("clips reset noconfirm"); 32 | }); 33 | 34 | // Event callback - inlineScripts.onExapsion 35 | confirmObjectPath( 36 | "_inlineScripts.inlineScripts.listeners.onExpansion.clips", 37 | (expansionInfo) => 38 | { 39 | _inlineScripts.clips.priorExpansion = 40 | expansionInfo.expansionText; 41 | }); 42 | ``` 43 | __ 44 | Sets up this shortcut-file 45 | 46 | 47 | __ 48 | ``` 49 | ^sfile shutdown$ 50 | ``` 51 | __ 52 | ```js 53 | // Event callback removal 54 | delete _inlineScripts.inlineScripts?.listeners?.onExpansion?.clips; 55 | 56 | // State removal 57 | delete _inlineScripts.state?.listeners?.onReset?.clips; 58 | ``` 59 | __ 60 | Shuts down this shortcut-file 61 | 62 | 63 | __ 64 | ``` 65 | ^clips reset?$ 66 | ``` 67 | __ 68 | ```js 69 | // Confirm 70 | if (!popups.confirm("Confirm resetting the Clips system")) { return null; } 71 | 72 | // Reset 73 | expand("clips reset noconfirm"); 74 | 75 | return expFormat("All clips cleared."); 76 | ``` 77 | __ 78 | clips reset - Removes all clips. 79 | *** 80 | 81 | 82 | __ 83 | ``` 84 | ^clips? reset noconfirm$ 85 | ``` 86 | __ 87 | ```js 88 | // Reset state object 89 | _inlineScripts.state.sessionState.clips = {}; 90 | ``` 91 | __ 92 | hidden - No-confirm reset 93 | 94 | 95 | __ 96 | ``` 97 | ^clips?$ 98 | ``` 99 | __ 100 | ```js 101 | // Expand to a list of clip names 102 | const clipNames = Object.keys(_inlineScripts.state.sessionState.clips); 103 | const result = "Clips:\n. " + (clipNames.length ? clipNames.join("\n. ") : "NONE"); 104 | 105 | return expFormat(result); 106 | ``` 107 | __ 108 | clips - Lists all stored clips. 109 | 110 | 111 | __ 112 | ``` 113 | ^clips? set ([_a-zA-Z][_a-zA-Z0-9]*) (.+)$ 114 | ``` 115 | __ 116 | ```js 117 | // Make clip titles case-insensitive 118 | $1 = $1.toLowerCase(); 119 | 120 | // Assign the clip contents to the clip name 121 | _inlineScripts.state.sessionState.clips[$1] = $2; 122 | 123 | return expFormat("Clip __" + $1 + "__ set to:\n" + $2); 124 | ``` 125 | __ 126 | clips set {name: name text} {value: text} - Creates / Sets a clip named {name} to the string {value}. 127 | 128 | 129 | __ 130 | ``` 131 | ^(?:clips? get|cg) ([_a-zA-Z][_a-zA-Z0-9]*)$ 132 | ``` 133 | __ 134 | ```js 135 | // Make clip titles case-insensitive 136 | $1 = $1.toLowerCase(); 137 | 138 | // Expand to the text associated with the clip name (or empty string for invalid name) 139 | return _inlineScripts.state.sessionState.clips[$1] || ""; 140 | ``` 141 | __ 142 | clips get {name: name text} - Expands to the value stored in clip {name}. 143 | - Alternative: __cg {name: name text}__ 144 | 145 | 146 | __ 147 | ``` 148 | ^clips? expansion ([_a-zA-Z][_a-zA-Z0-9]*)$ 149 | ``` 150 | __ 151 | ```js 152 | // Make clip titles case-insensitive 153 | $1 = $1.toLowerCase(); 154 | 155 | // Assign prior expansion text to the clip name 156 | _inlineScripts.state.sessionState.clips[$1] = _inlineScripts.clips.priorExpansion; 157 | 158 | return expFormat( 159 | "Clip __" + $1 + "__ set to:\n" + _inlineScripts.clips.priorExpansion); 160 | ``` 161 | __ 162 | clips expansion {name: name text} - Creates a clip named {name} that stores the previous expansion. 163 | 164 | 165 | __ 166 | ``` 167 | ^clips? remove ([_a-zA-Z][_a-zA-Z0-9]*)$ 168 | ``` 169 | __ 170 | ```js 171 | // Make clip titles case-insensitive 172 | $1 = $1.toLowerCase(); 173 | 174 | // If clip exists, remove it and notify user 175 | if (_inlineScripts.state.sessionState.clips[$1]) 176 | { 177 | delete _inlineScripts.state.sessionState.clips[$1]; 178 | return expFormat("Clip __" + $1 + "__ removed."); 179 | } 180 | 181 | // If clip doesn't exist, just notify user 182 | else 183 | { 184 | return expFormat("Clip __" + $1 + "__ not removed. Does not exist."); 185 | } 186 | ``` 187 | __ 188 | clips remove {name: name text} - Removes the clip named {name}. 189 | -------------------------------------------------------------------------------- /mythicv2_ui.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | An extension to __mythicv2.sfile__ that provides graphical ui versions of shortcuts. 6 | 7 | 8 | __ 9 | ``` 10 | ^sfile setup$ 11 | ``` 12 | __ 13 | ```js 14 | // Block loading if mythicgme is already setup (incompatible shortcut-files) 15 | if (_inlineScripts.inlineScripts.sfileIndices["mythicgme"]) 16 | { 17 | return true; 18 | } 19 | ``` 20 | __ 21 | Sets up this shortcut-file 22 | 23 | 24 | __ 25 | __ 26 | ```js 27 | // Reset the details if the current shortcut is user-triggered (i.e. a new action) 28 | function clearDetailsIfUserTriggered() 29 | { 30 | if (expansionInfo.isUserTriggered) 31 | { 32 | _inlineScripts.mythicv2.details = []; 33 | } 34 | } 35 | ``` 36 | __ 37 | Helper scripts 38 | 39 | 40 | __ 41 | ``` 42 | ^ui mythicv2 details$ 43 | ``` 44 | __ 45 | ```js 46 | // Start a new details list 47 | clearDetailsIfUserTriggered(); 48 | 49 | // Get the current showDetails flag state 50 | const currentState = _inlineScripts.state.sessionState.mythicv2.showDetails; 51 | 52 | // Choose a new showDetails flag value 53 | const pick = popups.pick( 54 | "Set 'details' mode", 55 | [ "Disable", "Enable" ], (currentState ? 1 : 0), 2); 56 | if (pick === null) { return null; } 57 | 58 | return expand("mythicv2 details " + (pick ? "y" : "n")); 59 | ``` 60 | __ 61 | ui mythicv2 details - Asks the user whether to enable or disable 'details' mode. 62 | Displays whether 'details' mode is enabled. 63 | 64 | 65 | __ 66 | ``` 67 | ^ui fate$ 68 | ``` 69 | __ 70 | ```js 71 | // Start a new details list 72 | clearDetailsIfUserTriggered(); 73 | 74 | // Data 75 | const odds = 76 | [ 77 | "Impossible", "No way", "Very unlikely", 78 | "Unlikely", "50 / 50", "likely", 79 | "Very likely", "Sure thing", "Has to be" 80 | ]; 81 | 82 | // Choose what odds to use in the fate check 83 | const pick = popups.pick("Choose the odds", odds, 4, 9); 84 | if (pick === null) { return null; } 85 | 86 | // Choose whether the pc benefits from a "yes" or a "no" 87 | const pick2 = 88 | popups.pick("What answer is best for the pc(s)?", [ "YES", "NO" ], 0, 2); 89 | if (pick2 === null) { return null; } 90 | 91 | return expand( "fate " + (pick - 4) + " " + (pick2 ? "n" : "y") ); 92 | ``` 93 | __ 94 | ui fate - Asks the user to choose the odds for this fate check. 95 | Asks the user what answer is best for the pc(s). 96 | Displays the answer to a yes/no question, possibly with a random event attached. 97 | 98 | 99 | __ 100 | ``` 101 | ^ui scene$ 102 | ``` 103 | __ 104 | ```js 105 | // Start a new details list 106 | clearDetailsIfUserTriggered(); 107 | 108 | // Choose a chaos adjustment 109 | const pick = popups.pick( 110 | "How was the previous scene?", [ "More controlled", "More chaotic" ], 1, 2); 111 | if (pick === null) { return null; } 112 | 113 | return expand("scene " + (pick * 2 - 1)); 114 | ``` 115 | __ 116 | ui scene - Asks the user to choose whether chaos increased or decreased last scene. 117 | Starts a new scene with the chosen chaos value adjustment. 118 | 119 | 120 | __ 121 | ``` 122 | ^ui disposition$ 123 | ``` 124 | __ 125 | ```js 126 | // Start a new details list 127 | clearDetailsIfUserTriggered(); 128 | 129 | // Choose the descriptor modifier value 130 | const choices = 131 | [ 132 | "-3 (ALL descriptors make the npc LESS ACTIVE)", "-2", "-1", 133 | "0", "+1", "+2", 134 | "+3 (ALL descriptors make the npc MORE ACTIVE)" 135 | ]; 136 | const pick = popups.pick("Enter the total descriptor modifier for the npc.\n\nConsider the npc's three descriptors in the current situation.\n+1 for descriptors that make the npc MORE active.\n-1 for descriptors that make the npc LESS active.\n0 for descriptors that don't affect npc activity.", choices, 3, 7); 137 | if (pick === null) { return null; } 138 | 139 | // Choose the base disposition 140 | const choices2 = 141 | [ 142 | "Random","2","3","4","5","6","7","8","9", 143 | "10","11","12","13","14","15","16","17", 144 | "18","19","20" 145 | ]; 146 | const pick2 = popups.pick("Enter the base disposition for the npc for this scene.\n\nIf the npc has a base dispositon for this scene, select it.\nIf the npc does NOT yet have a base disposition for this scene, select 'Random'.", choices2, 0, 20); 147 | if (pick2 === null) { return null; } 148 | 149 | // Base disposition is random, use the shortcut that rolls it 150 | if (!pick2) 151 | { 152 | return expand("disposition " + (pick - 3)); 153 | } 154 | 155 | // Base disposition is not random, use the shortcut that takes it as a parameter 156 | else 157 | { 158 | return expand("disposition " + (pick - 3) + " " + choices2[pick2]); 159 | } 160 | ``` 161 | __ 162 | ui disposition - Asks the user for the total descriptor modifier for an npc (+1 for descriptors that make the npc more active in the scene, -1 for descriptors that make the npc less active). 163 | If the npc already has a disposition for this scene, select it. If NOT, select "random" to choose one. 164 | Rolls and shows a disposition for the npc for the current scene. 165 | 166 | 167 | __ 168 | ``` 169 | ^ui action$ 170 | ``` 171 | __ 172 | ```js 173 | // Start a new details list 174 | clearDetailsIfUserTriggered(); 175 | 176 | // Choose the disposition modifier 177 | const dispositionMods = 178 | [ 179 | "-2 (Passive)", "0 (Moderate)", "+2 (Active)", 180 | "+4 (Aggressive)" 181 | ]; 182 | const pick = 183 | popups.pick("Enter the npc's current disposition modifier.", dispositionMods, 1, 184 | 4); 185 | if (pick === null) { return null; } 186 | 187 | return expand("action " + (pick * 2 - 2)); 188 | ``` 189 | __ 190 | ui action - Asks the user for the npc's disposition modifier. 191 | Rolls and shows an action for the npc to take. 192 | -------------------------------------------------------------------------------- /arrows.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts for writing various arrows: 6 | - →←↑↓ 7 | - ▶◀▲▼ 8 | - ⇒⇐⇑⇓ 9 | - ↔↕⇔⇕⇄⇅ 10 | - ↗↘↖↙ 11 | - ↪↩↻↺↝↜ 12 | 13 | 14 | __ 15 | ``` 16 | ^(?:arrow right|\-\>)$ 17 | ``` 18 | __ 19 | ```js 20 | return "→"; 21 | ``` 22 | __ 23 | -> - → (RIGHT arrow). 24 | - Alternative: __arrow right__ 25 | 26 | 27 | __ 28 | ``` 29 | ^(?:arrow left|\<\-)$ 30 | ``` 31 | __ 32 | ```js 33 | return "←"; 34 | ``` 35 | __ 36 | <- - ← (LEFT arrow). 37 | - Alternative: __arrow left__ 38 | 39 | 40 | __ 41 | ``` 42 | ^(?:arrow up|\-\^)$ 43 | ``` 44 | __ 45 | ```js 46 | return "↑"; 47 | ``` 48 | __ 49 | -^ - ↑ (UP arrow). 50 | - Alternative: __arrow up__ 51 | 52 | 53 | __ 54 | ``` 55 | ^(?:arrow down|\-v)$ 56 | ``` 57 | __ 58 | ```js 59 | return "↓"; 60 | ``` 61 | __ 62 | -v - ↓ (DOWN arrow). 63 | - Alternative: __arrow down__ 64 | *** 65 | 66 | 67 | __ 68 | ``` 69 | ^(?:tri right|\|\>)$ 70 | ``` 71 | __ 72 | ```js 73 | return "▶"; 74 | ``` 75 | __ 76 | |> - ▶ (RIGHT triangle). 77 | - Alternative: __tri right__ 78 | 79 | 80 | __ 81 | ``` 82 | ^(?:tri left|\<\|)$ 83 | ``` 84 | __ 85 | ```js 86 | return "◀"; 87 | ``` 88 | __ 89 | <| - ◀ (LEFT triangle). 90 | - Alternative: __tri left__ 91 | 92 | 93 | __ 94 | ``` 95 | ^(?:tri up|\|\^)$ 96 | ``` 97 | __ 98 | ```js 99 | return "▲"; 100 | ``` 101 | __ 102 | |^ - ▲ (UP triangle). 103 | - Alternative: __tri up__ 104 | 105 | 106 | __ 107 | ``` 108 | ^(?:tri down|\|v)$ 109 | ``` 110 | __ 111 | ```js 112 | return "▼"; 113 | ``` 114 | __ 115 | |v - ▼ (DOWN triangle). 116 | - Alternative: __tri down__ 117 | *** 118 | 119 | 120 | __ 121 | ``` 122 | ^(?:arrow dbl right|\=\>)$ 123 | ``` 124 | __ 125 | ```js 126 | return "⇒"; 127 | ``` 128 | __ 129 | => - ⇒ (RIGHT double-arrow). 130 | - Alternative: __arrow dbl right__ 131 | 132 | 133 | __ 134 | ``` 135 | ^(?:arrow dbl left|\<\=)$ 136 | ``` 137 | __ 138 | ```js 139 | return "⇐"; 140 | ``` 141 | __ 142 | <= - ⇐ (LEFT double-arrow). 143 | - Alternative: __arrow dbl left__ 144 | 145 | 146 | __ 147 | ``` 148 | ^(?:arrow dbl up|\=\^)$ 149 | ``` 150 | __ 151 | ```js 152 | return "⇑"; 153 | ``` 154 | __ 155 | =^ - ⇑ (UP double-arrow). 156 | - Alternative: __arrow dbl up__ 157 | 158 | 159 | __ 160 | ``` 161 | ^(?:arrow dbl down|\=v)$ 162 | ``` 163 | __ 164 | ```js 165 | return "⇓"; 166 | ``` 167 | __ 168 | =v - ⇓ (DOWN double-arrow). 169 | - Alternative: __arrow dbl down__ 170 | *** 171 | 172 | 173 | __ 174 | ``` 175 | ^(?:arrow leftright|\<\-\>)$ 176 | ``` 177 | __ 178 | ```js 179 | return "↔"; 180 | ``` 181 | __ 182 | <-> - ↔ (LEFT / RIGHT arrow). 183 | - Alternative: __arrow leftright__ 184 | 185 | 186 | __ 187 | ``` 188 | ^(?:arrow updown|\^\-v)$ 189 | ``` 190 | __ 191 | ```js 192 | return "↕"; 193 | ``` 194 | __ 195 | ^-v - ↕ (UP / DOWN arrow). 196 | - Alternative: __arrow updown__ 197 | 198 | 199 | __ 200 | ``` 201 | ^(?:arrow dbl leftright|\<\=\>)$ 202 | ``` 203 | __ 204 | ```js 205 | return "⇔"; 206 | ``` 207 | __ 208 | <=> - ⇔ (LEFT / RIGHT double arrow). 209 | - Alternative: __arrow dbl leftright__ 210 | 211 | 212 | __ 213 | ``` 214 | ^(?:arrow dbl updown|\^\=v)$ 215 | ``` 216 | __ 217 | ```js 218 | return "⇕"; 219 | ``` 220 | __ 221 | ^=v - ⇕ (UP / DOWN double arrow). 222 | - Alternative: __arrow dbtl updown__ 223 | 224 | 225 | __ 226 | ``` 227 | ^(?:arrow left right|\<\/\/\>)$ 228 | ``` 229 | __ 230 | ```js 231 | return "⇄"; 232 | ``` 233 | __ 234 | - ⇄ (LEFT arrow & RIGHT arrow). 235 | - Alternative: __arrow left right__ 236 | 237 | 238 | __ 239 | ``` 240 | ^(?:arrow up down|\^\/\/v)$ 241 | ``` 242 | __ 243 | ```js 244 | return "⇅"; 245 | ``` 246 | __ 247 | ^//v - ⇅ (UP arrow & DOWN arrow). 248 | - Alternative: __arrow up down__ 249 | *** 250 | 251 | 252 | __ 253 | ``` 254 | ^(?:arrow rightup|\-\^\>)$ 255 | ``` 256 | __ 257 | ```js 258 | return "↗"; 259 | ``` 260 | __ 261 | -^> - ↗ (RIGHT / UP arrow). 262 | - Alternative: __arrow rightup__ 263 | 264 | 265 | __ 266 | ``` 267 | ^(?:arrow rightdown|\-v\>)$ 268 | ``` 269 | __ 270 | ```js 271 | return "↘"; 272 | ``` 273 | __ 274 | -v> - ↘ (RIGHT / DOWN arrow). 275 | - Alternative: __arrow rightdown__ 276 | 277 | 278 | __ 279 | ``` 280 | ^(?:arrow leftup|\<\^\-)$ 281 | ``` 282 | __ 283 | ```js 284 | return "↖"; 285 | ``` 286 | __ 287 | <^- - ↖ (LEFT / UP arrow). 288 | - Alternative: __arrow leftup__ 289 | 290 | 291 | __ 292 | ``` 293 | ^(?:arrow leftdown|\)$ 308 | ``` 309 | __ 310 | ```js 311 | return "↪"; 312 | ``` 313 | __ 314 | u> - ↪ (RIGHT curve arrow). 315 | - Alternative: __arrow curve right__ 316 | 317 | 318 | __ 319 | ``` 320 | ^(?:arrow curve left|\)$ 334 | ``` 335 | __ 336 | ```js 337 | return "↻"; 338 | ``` 339 | __ 340 | c> - ↻ (CLOCKWISE arrow). 341 | - Alternative: __arrow clock__ 342 | 343 | 344 | __ 345 | ``` 346 | ^(?:arrow cclock|\)$ 360 | ``` 361 | __ 362 | ```js 363 | return "↝"; 364 | ``` 365 | __ 366 | \~> - ↝ (RIGHT wavy arrow). 367 | - Alternative: __arrow wavy right__ 368 | 369 | 370 | __ 371 | ``` 372 | ^(?:arrow wavy left|\<\~)$ 373 | ``` 374 | __ 375 | ```js 376 | return "↜"; 377 | ``` 378 | __ 379 | <\~ - ↜ (LEFT wavy arrow). 380 | - Alternative: __arrow wavy left__ 381 | -------------------------------------------------------------------------------- /notepick.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts to pick X random notes from a folder of notes and to get their front matter data. 6 | 7 | This shortcut-file has a tutorial video available: 8 | - [Using the "notepick" shortcut-file to randomly pick notes & get variables](https://www.youtube.com/watch?v=G1mvl-VwbIQ) (runtime 3:22) 9 | 10 | 11 | __ 12 | ``` 13 | ^sfile setup$ 14 | ``` 15 | __ 16 | ```js 17 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 18 | 19 | // Setup the state 20 | confirmObjectPath("_inlineScripts.state.sessionState.notepick"); 21 | 22 | // Event callback - state.onReseet 23 | confirmObjectPath( 24 | "_inlineScripts.state.listeners.onReset.notepick", 25 | function() 26 | { 27 | expand("notepick reset noconfirm"); 28 | }); 29 | ``` 30 | __ 31 | Sets up this shortcut-file 32 | 33 | 34 | __ 35 | ``` 36 | ^sfile shutdown$ 37 | ``` 38 | __ 39 | ```js 40 | // Event callback removal 41 | delete _inlineScripts.state?.listeners?.onReset?.notepick; 42 | 43 | // State removal 44 | delete _inlineScripts.state?.sessionState?.notepick; 45 | ``` 46 | __ 47 | Shuts down this shortcut-file 48 | 49 | 50 | __ 51 | ``` 52 | ^notepick reset$ 53 | ``` 54 | __ 55 | ```js 56 | // Confirm 57 | if (!popups.confirm("Confirm resetting the Notepick system")) { return null; } 58 | 59 | // Reset 60 | expand("notepick reset noconfirm"); 61 | 62 | return expFormat("All notepicks cleared."); 63 | ``` 64 | __ 65 | notepick reset - Clears all picks. 66 | *** 67 | 68 | 69 | __ 70 | ``` 71 | ^notepick reset noconfirm$ 72 | ``` 73 | __ 74 | ```js 75 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 76 | 77 | // Restart the state 78 | confirmObjectPath("_inlineScripts.state.sessionState"); 79 | _inlineScripts.state.sessionState.notepick = {}; 80 | ``` 81 | __ 82 | hidden - No-confirm reset 83 | 84 | 85 | __ 86 | __ 87 | ```js 88 | // Make a roll from 1 to max. 89 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 90 | 91 | // Pick an item from array a. 92 | function aPick(a) { return a[roll(a.length)-1]; } 93 | ``` 94 | __ 95 | Helper scripts 96 | 97 | 98 | __ 99 | ``` 100 | ^notepick pickFromFolderAndGetPick ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ?([1-9][0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|) ?(.*)$ 101 | ``` 102 | __ 103 | ```js 104 | // Remove any quotes around the folder path 105 | $1 = $1.replace(/^"(.*)"$/, "$1") 106 | 107 | // Pick random items. If error, early-out. 108 | let result = expUnformat( 109 | expand("notepick pickFromFolder " + $1 + " " + $2 + " " + $3 + " " + $4)); 110 | if (!result[0]) { return expFormat(result); } 111 | 112 | // Return the list of items picked 113 | return expand("notepick getPick " + $3); 114 | ``` 115 | __ 116 | notepick pickFromFolderAndGetPick {folder name: path text} {pick count: >0, default: 1} {pick id: name text, default: ""} {to ignore: text ( | separated filenames)} - Combines the shortcuts "notepick pickFromFolder" and "notepick getPick". 117 | 118 | 119 | __ 120 | ``` 121 | ^notepick pickFromFolderAndGetFrontmatter ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ?([1-9][0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|) ?(.*)$ 122 | ``` 123 | __ 124 | ```js 125 | // Remove any quotes around the folder path 126 | $1 = $1.replace(/^"(.*)"$/, "$1") 127 | 128 | // Pick random items. If error, early-out. 129 | let result = expUnformat( 130 | expand("notepick pickFromFolder " + $1 + " " + $2 + " " + $3 + " " + $4)); 131 | if (!result[0]) { return expFormat(result); } 132 | 133 | // Get the front matter for the items. If error, early-out. 134 | result = expUnformat(expand("notepick frontmatter " + $3)); 135 | if (!result[0]) { return expFormat(result); } 136 | 137 | return result[1]; 138 | ``` 139 | __ 140 | notepick pickFromFolderAndGetFrontmatter {folder name: path text} {pick count: >0, default: 1} {pick id: name text, default: ""} {to ignore: text ( | separated filenames)} - Combines the shortcuts "notepick pickFromFolder" and "notepick frontmatter". 141 | *** 142 | 143 | 144 | __ 145 | ``` 146 | ^notepick pickFromFolder ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ?([1-9][0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|) ?(.*)$ 147 | ``` 148 | __ 149 | ```js 150 | // Remove any quotes around the folder path 151 | $1 = $1.replace(/^"(.*)"$/, "$1") 152 | // Count defaults to 1 153 | $2 = Number($2) || 1; 154 | // Split toIgnore into filenames 155 | $4 = $4.split("|"); 156 | 157 | // Get the file object for the given folder. Early out of doesn't exist or is a file 158 | const folder = app.vault.fileMap[$1]; 159 | if (!folder) 160 | { 161 | return expFormat([ "","No files picked. Folder __" + $1 + "__ doesn't exist." ]); 162 | } 163 | if (!folder.children) 164 | { 165 | return expFormat([ "", "No files picked. __" + $1 + "__ isn't a folder." ]); 166 | } 167 | 168 | // Get the non-folder children of folder 169 | let files = folder.children.filter(v => !v.children).map(v => v.path); 170 | 171 | // Remove the files excluded by the "to ignore" parameter 172 | files = files.filter(v => !$4.includes(v)); 173 | 174 | // If file count is under the pick count, early out. 175 | if (files.length < $2) 176 | { 177 | return expFormat( 178 | [ "", "No files picked. Not enough files in Folder __" + $1 + "__." ]); 179 | } 180 | 181 | // Randomly pick of the files. 182 | const pick = []; 183 | while (pick.length < $2) 184 | { 185 | const nextPickItem = aPick(files); 186 | if (!pick.includes(nextPickItem)) 187 | { 188 | pick.push(nextPickItem); 189 | } 190 | } 191 | 192 | // Add the files picked to the state 193 | _inlineScripts.state.sessionState.notepick[$3] = pick; 194 | 195 | return expFormat([ $2 + " file(s) picked" + ($3 ? " for " + $3 : "") + "." ]); 196 | ``` 197 | __ 198 | notepick pickFromFolder {folder name: path text} {count: >0, default: 1} {pick id: name text, default: ""} {to ignore: text ( | separated filenames)} - Picks {count} random notes from folder {folder name} and remembers them as {pick id}. Any files in {to ignore} are never picked. 199 | 200 | 201 | __ 202 | ``` 203 | ^notepick getPick ?([_a-zA-Z][_a-zA-Z0-9]*|)$ 204 | ``` 205 | __ 206 | ```js 207 | // Get the specified pick from the state. Early out if specified pick isn't stored. 208 | const picks = _inlineScripts.state.sessionState.notepick[$1]; 209 | if (!picks) 210 | { 211 | return expFormat([ "", "No pick. Pick id __" + $1 + "__ not found." ]); 212 | } 213 | 214 | return expFormat( 215 | [ "Pick" + ($1 ? " __" + $1 + "__" : "") + ":\n", picks.join("\n"), "" ]); 216 | ``` 217 | __ 218 | notepick getPick {pick id: name text, default: ""} - Gets a list of the files last picked for {pick id}. 219 | 220 | 221 | __ 222 | ``` 223 | ^notepick frontmatter ?([_a-zA-Z][_a-zA-Z0-9]*|)$ 224 | ``` 225 | __ 226 | ```js 227 | // Get the specified pick from the state. Early out if specified pick isn't stored. 228 | const pick = _inlineScripts.state.sessionState.notepick[$1]; 229 | if (!pick) 230 | { 231 | return expFormat( 232 | [ "", "No frontmatter gathered. Pick id __" + $1 + "__ not found." ]); 233 | } 234 | 235 | // Get the file objects of the picks 236 | const files = pick.map(v => app.vault.fileMap[v]); 237 | 238 | // Confirm that all picks have valid file objects. If not, early out. 239 | for (let i = 0; i < pick.length; i++) 240 | { 241 | if (!files[i]) 242 | { 243 | return expFormat( 244 | [ "", "No frontmatter gathered. __" + fileNotFound + "__ not found." ]); 245 | } 246 | } 247 | 248 | // Get the frontmatter for each of the picks. 249 | let result = {}; 250 | for (const file of files) 251 | { 252 | result[file.name.replace(/.md$/, "")] = 253 | app.metadataCache.getFileCache(file).frontmatter || {}; 254 | } 255 | 256 | return expFormat( 257 | [ "Frontmatter gathered for " + pick.length + " note(s).\n", result, "" ]); 258 | ``` 259 | __ 260 | notepick frontmatter {pick id: name text, default: ""} - Gets the frontmatter from the notes that are remembered in {pick id}. 261 | -------------------------------------------------------------------------------- /lipsum.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | A decent Lorum Ipsum generator. 6 | 7 | 8 | __ 9 | ``` 10 | ^sfile setup$ 11 | ``` 12 | __ 13 | ```js 14 | // Only do this once 15 | if (window._inlineScripts?.lipsum) { return; } 16 | 17 | // Data to make output text from 18 | let l = ` 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et sem quam. Proin viverra egestas sem, et finibus mi volutpat eget. Aliquam vel nulla mattis, malesuada lectus sed, sagittis eros. Etiam vulputate, felis nec aliquam tempor, velit ex lobortis nisi, ac aliquet magna arcu id velit. Nam odio ligula, consequat eget elit non, accumsan rutrum est. Mauris vitae ligula nec lacus finibus venenatis at molestie ligula. Ut suscipit ex et finibus gravida. 21 | 22 | Phasellus orci neque, dictum at dui vel, ultricies varius sapien. Praesent ultricies ultrices mi eget luctus. Integer porttitor purus sit amet ante luctus mollis. Nulla lorem diam, fringilla in rhoncus quis, vestibulum lacinia dolor. Nam finibus eros ut nisl pellentesque euismod. Nunc nec ipsum in purus dapibus cursus eu eu ex. Ut nec mauris neque. Sed eget enim ut velit laoreet vehicula quis quis sem. 23 | 24 | Nullam pulvinar hendrerit ipsum, nec gravida neque aliquam at. Suspendisse justo metus, rutrum ac ligula sed, varius rutrum sapien. Etiam porttitor arcu massa, non laoreet eros sodales a. Integer et est et dolor maximus feugiat. Nam tristique purus a nibh fringilla, sit amet porta libero dictum. Quisque rhoncus pharetra cursus. Pellentesque sed rutrum sapien, nec pretium nisi. Aenean vitae quam sit amet nisl venenatis congue ut non libero. Donec sit amet mattis tellus, et condimentum justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Maecenas tristique est nibh, ac dignissim quam pretium in. Nam semper odio eget tellus tempus, eu vestibulum tortor tempor. Curabitur elit ex, posuere nec diam eget, malesuada blandit arcu. Proin condimentum arcu sed lorem consectetur convallis. Sed facilisis convallis lorem, ut fermentum nulla pretium vel. 25 | 26 | Mauris pulvinar lectus eu mi vehicula, ut dapibus velit egestas. Nullam pellentesque a nibh quis sodales. Donec lacinia vulputate est. Vivamus lacinia neque a libero faucibus, sed condimentum dui imperdiet. In dignissim aliquet tellus a finibus. Cras suscipit tempus sagittis. In lobortis arcu quis ante sagittis cursus. Vestibulum vitae lectus sit amet felis placerat pulvinar ac id sapien. Suspendisse congue, sem vitae consectetur pharetra, libero metus congue quam, quis finibus felis eros eu arcu. 27 | 28 | Vestibulum sagittis tristique lectus sit amet accumsan. Sed laoreet lectus eu euismod scelerisque. In velit dolor, placerat id rhoncus aliquam, semper in lectus. Curabitur ut ante congue, ullamcorper ante vitae, accumsan quam. Phasellus ante leo, pulvinar eu leo quis, convallis elementum nisi. Praesent et imperdiet metus, sed aliquet arcu. Phasellus efficitur vehicula libero, quis efficitur arcu. Nulla maximus tellus non luctus auctor. 29 | 30 | Aliquam gravida condimentum nisl, id aliquam ipsum efficitur id. Quisque et mauris ac purus accumsan porta vel sit amet lectus. Duis viverra accumsan risus, nec fringilla ipsum posuere in. Donec dapibus nisl at congue facilisis. Nam malesuada accumsan dapibus. Vestibulum rhoncus ornare aliquam. Mauris viverra, diam sit amet fermentum venenatis, tortor leo ultrices massa, vel lobortis neque urna eget felis. Aliquam viverra erat ac dolor sagittis, nec aliquet lorem blandit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam molestie pellentesque laoreet. Duis nulla eros, lobortis vitae elit in, maximus facilisis felis. Proin viverra venenatis eros, nec consequat dui euismod eu. 31 | 32 | Nam vestibulum turpis ac lorem luctus, ut faucibus leo vehicula. Praesent laoreet nibh turpis, id tincidunt mauris ultricies ut. Integer euismod turpis sit amet nulla iaculis consequat. Vivamus cursus arcu ac sem luctus egestas. Integer varius lorem augue, ac consequat neque vehicula sit amet. Aliquam eu orci congue, porttitor est vitae, tincidunt quam. Phasellus vehicula, quam id ultricies aliquet, dolor risus tincidunt leo, sit amet eleifend nisi ex ut erat. Praesent porta vehicula nisl, ac feugiat dui viverra in. Nunc vehicula sagittis tortor, eget commodo erat hendrerit ac. Praesent accumsan laoreet nisi vitae faucibus. Proin in turpis id ante suscipit sodales. 33 | 34 | Vestibulum accumsan dignissim metus non cursus. Sed pulvinar imperdiet arcu, a condimentum ipsum sodales sit amet. Integer eu dapibus lacus. Duis id ligula et nunc scelerisque sagittis eget sed libero. Nulla id justo varius, pellentesque augue et, egestas sem. Donec cursus ligula in hendrerit suscipit. Nullam laoreet justo bibendum, mollis nibh sed, egestas ipsum. Aenean volutpat erat velit, vel placerat mauris efficitur eu. Aenean quam arcu, vehicula non commodo id, blandit a magna. Cras ac sem dolor. Nam tempor odio dictum, consequat augue in, finibus risus. Nunc elementum leo at maximus scelerisque. Nullam at feugiat ipsum, quis facilisis dolor. 35 | 36 | Aliquam nec sapien urna. Aliquam bibendum, ligula id lobortis elementum, odio nulla pretium justo, vitae maximus dolor elit vitae risus. Pellentesque suscipit mi eget augue hendrerit tincidunt. Phasellus ut pulvinar mi. Pellentesque euismod lobortis fermentum. Aliquam vitae lacus sit amet sapien malesuada tempus. Proin iaculis nunc sed ullamcorper rutrum. Nullam non interdum elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ac metus at tortor luctus vehicula id at dolor. Nulla pulvinar ornare venenatis. Vivamus a ultricies mauris, et maximus enim. Mauris blandit tellus id lacus placerat, ut blandit purus ultrices. 37 | 38 | Ut pharetra semper leo at convallis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Aenean vulputate, ex in imperdiet pharetra, leo libero mattis nunc, at tristique turpis libero ac lectus. Suspendisse potenti. Integer sed dolor at urna ultricies ornare. Ut auctor vulputate diam, sit amet condimentum lectus mattis at. Aenean placerat molestie arcu at pretium. Donec consequat nibh nulla, vel mollis lacus tincidunt et. 39 | 40 | ` 41 | 42 | // Break data into pieces to recombine for output text 43 | let pSizes = []; 44 | let sentences = []; 45 | l = l.trim().split("\n\n"); 46 | for (let i = 0; i < l.length; i++) 47 | { 48 | l[i] = l[i].slice(0,-1).split(". "); 49 | pSizes.push(l[i].length); 50 | sentences = sentences.concat(l[i]); 51 | } 52 | let first = sentences.shift(); 53 | window._inlineScripts ||= {}; 54 | window._inlineScripts.lipsum = 55 | { first: first, pSizes: pSizes, sentences: sentences }; 56 | ``` 57 | __ 58 | Takes a source Lorem Ipsum text, generated at https://lipsum.com/feed/html, and breaks it down into individual sentences and counts of sentences per paragraph. On user request, builds a Lorem Ipsum text of random sentences from the source, combined into paragraphs of random size based on the source's paragraphs. 59 | 60 | 61 | __ 62 | ``` 63 | ^lipsum ?(|[1-9][0-9]*)$ 64 | ``` 65 | __ 66 | ```js 67 | // Count parameter defaults to 1 68 | $1 = Number($1) || 1; 69 | 70 | // Helper function - Make a roll from 1 to max. 71 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 72 | 73 | // Helper function - Pick an item from array a. 74 | function aPick(a) { return a[roll(a.length)-1]; } 75 | 76 | // Generate and return text from the data created on setup 77 | let result = ""; 78 | for (let i = 0; i < $1; i++) 79 | { 80 | let pSize = aPick(window._inlineScripts.lipsum.pSizes); 81 | if (i === 0) 82 | { 83 | pSize--; 84 | result += window._inlineScripts.lipsum.first + ". "; 85 | } 86 | else 87 | { 88 | result += "\n\n"; 89 | } 90 | for (let k = 0; k < pSize; k++) 91 | { 92 | result += aPick(window._inlineScripts.lipsum.sentences) + ". "; 93 | } 94 | result = result.slice(0,-1); 95 | } 96 | return result; 97 | ``` 98 | __ 99 | lipsum {paragraph count: >0, default: 1} - Generates a lorem ipsum text with {paragraph count} paragraphs. 100 | -------------------------------------------------------------------------------- /une_ui.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | An extension to __une.sfile__ that provides graphical ui versions of shortcuts. 6 | 7 | 8 | __ 9 | ``` 10 | ^sfile setup$ 11 | ``` 12 | __ 13 | ```js 14 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 15 | 16 | _inlineScripts.inlineScripts.HelperFncs.addCss("une_ui", ".iscript_une_select { height: unset; padding: .25em; margin-top: .5em; margin-bottom: 1.5em } .iscript_popup { max-height: unset !important; }"); 17 | 18 | // Custom popop definition for the 3 possible parameters used in uni shortcuts 19 | confirmObjectPath("_inlineScripts.une_ui.uneInputPopup");//, 20 | _inlineScripts.une_ui.uneInputPopup = 21 | { 22 | // Setup function 23 | onOpen: async (data, parent, firstButton, SettingType) => 24 | { 25 | // Randomness 26 | if (data.getRandomness) 27 | { 28 | const titleUi = document.createElement("div"); 29 | titleUi.innerText = "Randomness"; 30 | titleUi.classList.add("setting-item-name"); 31 | parent.append(titleUi); 32 | const descriptionUi = document.createElement("div"); 33 | descriptionUi.innerText = 34 | "Choose the randomness of the scene (for Power Level)"; 35 | descriptionUi.classList.add("setting-item-description"); 36 | parent.append(descriptionUi); 37 | data.randomnessUi = document.createElement("select"); 38 | data.randomnessUi.innerHTML = 39 | "" + 40 | "" + 41 | "" + 42 | "" + 43 | ""; 44 | data.randomnessUi.size = 5; 45 | data.randomnessUi.classList.add("iscript_une_select"); 46 | data.randomnessUi.addEventListener("keypress", e => 47 | { 48 | if (e.key === "Enter") { firstButton.click(); } 49 | }) 50 | parent.append(data.randomnessUi); 51 | } 52 | 53 | // Relationship 54 | if (data.getRelationship) 55 | { 56 | const titleUi = document.createElement("div"); 57 | titleUi.innerText = "Relationship"; 58 | titleUi.classList.add("setting-item-name"); 59 | parent.append(titleUi); 60 | const descriptionUi = document.createElement("div"); 61 | descriptionUi.innerText = 62 | "Choose the NPC's feelings towards the PC(s) (for mood)"; 63 | descriptionUi.classList.add("setting-item-description"); 64 | parent.append(descriptionUi); 65 | data.relationshipUi = document.createElement("select"); 66 | data.relationshipUi.innerHTML = 67 | "" + 68 | "" + 69 | "" + 70 | "" + 71 | "" + 72 | "" + 73 | ""; 74 | data.relationshipUi.size = 7; 75 | data.relationshipUi.classList.add("iscript_une_select"); 76 | data.relationshipUi.addEventListener("keypress", e => 77 | { 78 | if (e.key === "Enter") { firstButton.click(); } 79 | }) 80 | parent.append(data.relationshipUi); 81 | } 82 | 83 | // Demeanor 84 | if (data.getDemeanor) 85 | { 86 | const titleUi = document.createElement("div"); 87 | titleUi.innerText = "Demeanor"; 88 | titleUi.classList.add("setting-item-name"); 89 | parent.append(titleUi); 90 | const descriptionUi = document.createElement("div"); 91 | descriptionUi.innerText = 92 | "Choose the NPC's demeanor (for bearing)"; 93 | descriptionUi.classList.add("setting-item-description"); 94 | parent.append(descriptionUi); 95 | data.demeanorUi = document.createElement("select"); 96 | data.demeanorUi.innerHTML = 97 | "" + 98 | "" + 99 | "" + 100 | "" + 101 | "" + 102 | "" + 103 | "" + 104 | "" + 105 | ""; 106 | data.demeanorUi.size = 9; 107 | data.demeanorUi.classList.add("iscript_une_select"); 108 | data.demeanorUi.addEventListener("keypress", e => 109 | { 110 | if (e.key === "Enter") { firstButton.click(); } 111 | }) 112 | parent.append(data.demeanorUi); 113 | } 114 | }, 115 | 116 | // Shutown function 117 | onClose: async (data, resolveFnc, buttonId) => 118 | { 119 | if (buttonId !== "Ok") { return; } 120 | 121 | // Setup and return results 122 | let result = {}; 123 | if (data.getRandomness) 124 | { 125 | result.randomness = Number(data.randomnessUi.value); 126 | } 127 | if (data.getRelationship) 128 | { 129 | result.relationship = Number(data.relationshipUi.value); 130 | } 131 | if (data.getDemeanor) 132 | { 133 | result.demeanor = Number(data.demeanorUi.value); 134 | } 135 | resolveFnc(result); 136 | } 137 | };//); 138 | ``` 139 | __ 140 | Sets up this shortcut-file 141 | 142 | 143 | __ 144 | ``` 145 | ^sfile shutdown$ 146 | ``` 147 | __ 148 | ```js 149 | // State removal 150 | delete _inlineScripts.une_ui; 151 | 152 | // Custom CSS removal 153 | _inlineScripts.inlineScripts.HelperFncs.removeCss("une_ui"); 154 | ``` 155 | __ 156 | Shuts down this shortcut-file 157 | 158 | 159 | __ 160 | ``` 161 | ^ui une$ 162 | ``` 163 | __ 164 | ```js 165 | // Get input from the custom popup for une parameters 166 | const input = popups.custom( 167 | "UNE generator - Full", 168 | _inlineScripts.une_ui.uneInputPopup, 169 | { getRandomness: true, getRelationship: true, getDemeanor: true }); 170 | if (!input) { return null; } 171 | 172 | return expand( 173 | "une " + input.randomness + " " + input.relationship + 174 | (input.demeanor ? (" " + input.demeanor) : "")); 175 | ``` 176 | __ 177 | ui une - Asks the user to choose the scene's randomness (for picking the NPC's power). 178 | Asks the user to choose the NPC's felings towards the PC(s) (for the NPC's mood). 179 | Asks the user to choose the NPC's demeanor, or pick it randomly (for the NPC's bearing). 180 | Displays the NPC's character (identity, power, motive) and the NPC's interaction for this scene (mood, bearing, focus). 181 | *** 182 | 183 | 184 | __ 185 | ``` 186 | ^ui une character$ 187 | ``` 188 | __ 189 | ```js 190 | // Get input from the custom popup for une parameters 191 | const input = popups.custom( 192 | "UNE generator - Character", 193 | _inlineScripts.une_ui.uneInputPopup, 194 | { getRandomness: true, getRelationship: false, getDemeanor: false }); 195 | if (!input) { return null; } 196 | 197 | return expand("une character " + input.randomness); 198 | ``` 199 | __ 200 | ui une character - Asks the user to choose the scene's randomness (for picking the NPC's power). 201 | Displays the NPC's character (identity, power, motive). 202 | 203 | 204 | __ 205 | ``` 206 | ^ui une interact$ 207 | ``` 208 | __ 209 | ```js 210 | // Get input from the custom popup for une parameters 211 | const input = popups.custom( 212 | "UNE generator - Iteraction", 213 | _inlineScripts.une_ui.uneInputPopup, 214 | { getRandomness: false, getRelationship: true, getDemeanor: true }); 215 | if (!input) { return null; } 216 | 217 | return expand( 218 | "une interact " + input.relationship + 219 | (input.demeanor ? (" " + input.demeanor) : "")); 220 | ``` 221 | __ 222 | ui une interact - Asks the user to choose the NPC's feelings towards the PC(s) (for the NPC's mood). 223 | Asks the user to choose the NPC's demeanor, or pick it randomly (for the NPC's bearing). 224 | Displays the NPC's interaction for this scene (mood, bearing, focus). 225 | *** 226 | 227 | 228 | __ 229 | ``` 230 | ^ui une identity$ 231 | ``` 232 | __ 233 | ```js 234 | // Just wraps "une identity" 235 | return expand("une identity"); 236 | ``` 237 | __ 238 | ui une identity - Generates a 2-word description for an NPC. 239 | 240 | 241 | __ 242 | ``` 243 | ^ui une power$ 244 | ``` 245 | __ 246 | ```js 247 | // Get input from the custom popup for une parameters 248 | const input = popups.custom( 249 | "UNE generator - Power", 250 | _inlineScripts.une_ui.uneInputPopup, 251 | { getRandomness: true, getRelationship: false, getDemeanor: false }); 252 | if (!input) { return null; } 253 | 254 | return expand("une power " + input.randomness); 255 | ``` 256 | __ 257 | ui une power - Asks the user to choose the scene's randomness. 258 | Displays the NPC's power relative to the PC power(s). 259 | 260 | 261 | __ 262 | ``` 263 | ^ui une motive$ 264 | ``` 265 | __ 266 | ```js 267 | // Just wraps "une motive" 268 | return expand("une motive"); 269 | ``` 270 | __ 271 | ui une motive - Generates three 2-word descriptions for an NPC's motivations. 272 | *** 273 | 274 | 275 | __ 276 | ``` 277 | ^ui une mood$ 278 | ``` 279 | __ 280 | ```js 281 | // Get input from the custom popup for une parameters 282 | const input = popups.custom( 283 | "UNE generator - Mood", 284 | _inlineScripts.une_ui.uneInputPopup, 285 | { getRandomness: false, getRelationship: true, getDemeanor: false }); 286 | if (!input) { return null; } 287 | 288 | return expand("une mood " + input.relationship); 289 | ``` 290 | __ 291 | ui une mood - Asks the user to choose the NPC's felings towards the PC(s). 292 | Displays the NPC's mood for this scene. 293 | 294 | 295 | __ 296 | ``` 297 | ^ui une bearing$ 298 | ``` 299 | __ 300 | ```js 301 | // Get input from the custom popup for une parameters 302 | const input = popups.custom( 303 | "UNE generator - Bearing", 304 | _inlineScripts.une_ui.uneInputPopup, 305 | { getRandomness: false, getRelationship: false, getDemeanor: true }); 306 | if (!input) { return null; } 307 | 308 | return expand("une bearing" + (input.demeanor ? (" " + input.demeanor) : "")); 309 | ``` 310 | __ 311 | ui une bearing - Asks the user to choose the NPC's demeanor, or pick it randomly. 312 | Displays a the NPC's bearing. 313 | 314 | 315 | __ 316 | ``` 317 | ^ui une focus$ 318 | ``` 319 | __ 320 | ```js 321 | // Just wraps "une focus" 322 | return expand("une focus"); 323 | ``` 324 | __ 325 | ui une focus - Generates an NPC's primary interest for this interaction. -------------------------------------------------------------------------------- /state.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | This shortcut-file works with the state of many other shortcut-files. Notably, it automatically maintains their state between Obsidian sessions. It also includes shortcuts to allow you to save, load and reset states. 6 | 7 | This shortcut file can be used by other shortcut-files to let them setup their own state that persists across Obsidian sessions. 8 | 9 | This shortcut-file has 2 tutorial videos available. The first video is the best for most users, but if you are writing shortcuts, then the second video shows you how to setup your own shortcut-file to use the state system: 10 | - [Using the "state" shortcut-file to save and load session state](https://www.youtube.com/watch?v=IiRHB0FfJcI) (runtime 2:45) 11 | - [Setup a shortcut-file for saving & loading with the "state" shortcut-file](https://www.youtube.com/watch?v=K-6Xy3YLuh4) (runtime 6:12) 12 | 13 | 14 | To save the session state, use the "state get" shortcut. This expands into a "state-string"- a string which includes all data in the current session state. You can save this state-string simply by leaving it as part of the note. To load the session state, call the "state set" shortcut, passing in a state-string _(created earlier with the "state get" shortcut)_. 15 | 16 | Alternately, "state set last" will load the state-string from the last call to the "state get" shortcut in the current document. 17 | 18 | 19 | __ 20 | ``` 21 | ^sfile setup$ 22 | ``` 23 | __ 24 | ```js 25 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 26 | 27 | // Prevent running this shortcut multiple times concurrently. There are race- 28 | // condition in the file handling. 29 | if (_inlineScripts?.state?.inSetup) { return; } 30 | confirmObjectPath("_inlineScripts.state.inSetup", true); 31 | 32 | // The name of the state data-file - the file to store the state in 33 | const STATE_FILE_NAME = "Ξ_state.data.md"; 34 | 35 | // Helper function - Remomve file without triggering an exception if it doesn't exist 36 | async function removeFile(filename) 37 | { 38 | if (await app.vault.adapter.exists(filename)) 39 | { 40 | await app.vault.adapter.remove(filename); 41 | } 42 | } 43 | 44 | // Setup the event callback arrays 45 | confirmObjectPath("_inlineScripts.state.listeners.onReset"); 46 | confirmObjectPath("_inlineScripts.state.listeners.onLoad"); 47 | 48 | // Determine root folder for state data-file (every time, in case sfiles are moved) 49 | confirmObjectPath("_inlineScripts.state.stateFile"); 50 | let stateFileHasBeenSet = false; 51 | // Find the "state" sfile in the sfile's list... 52 | for (const sfile of _inlineScripts.inlineScripts.plugin.settings.shortcutFiles) 53 | { 54 | // Found the "state" sfile! 55 | if (sfile.address.endsWith("state.sfile.md")) 56 | { 57 | // Make the state data-file be stored right next to the state sfile 58 | const path = sfile.address.slice(0, -14) + STATE_FILE_NAME; 59 | // If the new path is different, change it and remember that it's been changed 60 | if (_inlineScripts.state.stateFile.path != path) 61 | { 62 | _inlineScripts.state.stateFile.path = path; 63 | stateFileHasBeenSet = true; 64 | } 65 | break; 66 | } 67 | } 68 | 69 | // Handle old versions of state data-file locations by finding the latest one and 70 | // copying it to the new state data-file. Then remove all existing old version files. 71 | // NOTE - this block is to upgrade old vaults to the latest state system version. 72 | if (!_inlineScripts.state.sessionState) 73 | { 74 | const oldStateFilePossibilities = 75 | [ 76 | ".obsidian/plugins/obsidian-text-expander-js/state.data.txt", 77 | ".obsidian/plugins/obsidian-text-expander-js/state_onquit.data.txt", 78 | ".obsidian/plugins/obsidian-text-expander-js/state_auto.data.txt" 79 | ]; 80 | let oldStateFiles = []; 81 | for (let i = oldStateFilePossibilities.length - 1; i >= 0; i--) 82 | { 83 | if (await app.vault.adapter.exists(oldStateFilePossibilities[i])) 84 | { 85 | oldStateFiles.push(oldStateFilePossibilities[i]); 86 | } 87 | } 88 | if (oldStateFiles.length > 1) 89 | { 90 | let times = []; 91 | for (let i = 0; i < oldStateFiles.length; i++) 92 | { 93 | times.push((await app.vault.adapter.stat(oldStateFiles[i])).mtime); 94 | } 95 | let latest = 0; 96 | let latestTime = times[0]; 97 | for (let i = 1; i < oldStateFiles.length; i++) 98 | { 99 | if (times[i] > latestTime) 100 | { 101 | latest = i; 102 | latestTime = times[i]; 103 | } 104 | } 105 | oldStateFiles = [ oldStateFiles[latest] ]; 106 | } 107 | if (oldStateFiles.length == 1) 108 | { 109 | // Remove current state file (if it exists) 110 | await removeFile(_inlineScripts.state.stateFile.path); 111 | // Copy the latest old state file into the current state file 112 | await app.vault.adapter.copy( 113 | oldStateFiles[0], 114 | _inlineScripts.state.stateFile.path); 115 | stateFileHasBeenSet = true; 116 | // Delete oldStateFilePossibilities 117 | for (const oldStateFilePossibility of oldStateFilePossibilities) 118 | { 119 | await removeFile(oldStateFilePossibility); 120 | } 121 | } 122 | } 123 | 124 | // Load the state file if running initial setup, or if the state file has been moved 125 | if (!_inlineScripts.state.sessionState || stateFileHasBeenSet) 126 | { 127 | try 128 | { 129 | // Get the raw content 130 | const stateString = 131 | await app.vault.adapter.read(_inlineScripts.state.stateFile.path); 132 | 133 | // Parse the content into the state 134 | _inlineScripts.state.sessionState = JSON.parse(stateString); 135 | 136 | // Assign raw content to the "priorString" to recognize when state has changed 137 | _inlineScripts.state.stateFile.priorString = stateString; 138 | 139 | // Call event callbacks for the state.onLoad event 140 | _inlineScripts.inlineScripts.HelperFncs.callEventListenerCollection( 141 | "state.onLoad", _inlineScripts.state.listeners.onLoad); 142 | } 143 | catch (e) 144 | { 145 | // If parsing failed, create a console error 146 | if (e.code !== "ENOENT") 147 | { 148 | console.error( 149 | "state.sfile\n\tUnable to load from state file:\n\t" + 150 | _inlineScripts.state.stateFile.path + "\n\t", e); 151 | } 152 | // Make sure we have SOMETHING for the state 153 | _inlineScripts.state.sessionState ||= {}; 154 | // Trigger a state save to make sure the state data-file is up to date 155 | expand("state save"); 156 | } 157 | } 158 | 159 | // Event callback - inlineScripts.onExpansion - React to each user-triggered shortcut 160 | // expansion by checking for state changes and re-saving if there are any. 161 | confirmObjectPath( 162 | "_inlineScripts.inlineScripts.listeners.onExpansion.state", 163 | async (expansionInfo) => 164 | { 165 | // Only re-save if the expansion is user-triggered 166 | if (expansionInfo.isUserTriggered) 167 | { 168 | // Check for state changes and re-save if there are any. 169 | expand("state save"); 170 | } 171 | }); 172 | 173 | // End the block to running this shortcut multiple times concurrently. 174 | _inlineScripts.state.inSetup = false; 175 | ``` 176 | __ 177 | Sets up this shortcut-file 178 | 179 | 180 | __ 181 | ``` 182 | ^sfile shutdown$ 183 | ``` 184 | __ 185 | ```js 186 | // State removal 187 | delete _inlineScripts.state; 188 | ``` 189 | __ 190 | Shuts down this shortcut-file 191 | 192 | 193 | __ 194 | ``` 195 | ^state reset$ 196 | ``` 197 | __ 198 | ```js 199 | // Confirm 200 | if (!popups.confirm("Confirm resetting the State system")) { return null; } 201 | if (!popups.confirm( 202 | "This will clear the state for all shortcut files!
Are you sure?")) 203 | { 204 | return null; 205 | } 206 | 207 | // Wipe out all states 208 | _inlineScripts.state.sessionState = {}; 209 | 210 | // Notify listeners of state.onReset event 211 | _inlineScripts.inlineScripts.HelperFncs.callEventListenerCollection( 212 | "state.onReset", _inlineScripts.state.listeners.onReset); 213 | 214 | return expFormat("All state cleared."); 215 | ``` 216 | __ 217 | state reset - Clears all session state. 218 | 219 | 220 | __ 221 | __ 222 | ```js 223 | // The part of the "state get" expansion that is not the state data-string. 224 | // Used in "state get" and "state restore" shortcuts. 225 | const GET_MSG_PREFIX = "State:\n"; 226 | ``` 227 | __ 228 | Helper script 229 | 230 | 231 | __ 232 | ``` 233 | ^state get$ 234 | ``` 235 | __ 236 | ```js 237 | // Return the state data-string. 238 | // NOTE: don't use the expansion-format's prefix or line-prefix. This makes the 239 | // data-string is easier to select and copy in the note. 240 | return expFormat( 241 | GET_MSG_PREFIX + JSON.stringify(_inlineScripts.state.sessionState), true, true); 242 | ``` 243 | __ 244 | state get - Expands to a state-string - a string containing all data for the current session state. 245 | 246 | 247 | __ 248 | ``` 249 | ^state restore$ 250 | ``` 251 | __ 252 | ```js 253 | // Get the content of the current note 254 | const content = await app.vault.cachedRead( app.workspace.getActiveFile()); 255 | 256 | // Find the last expansion of "state get" in the content. Early out if no expansions 257 | const getMsgIndex = content.lastIndexOf(GET_MSG_PREFIX); 258 | if (getMsgIndex < 0) 259 | { 260 | return expFormat("State not loaded. Last state not found."); 261 | } 262 | 263 | // Get the end index of the data-string in the content. Early out if endIndex failed 264 | const endIndex = content.indexOf("\n", getMsgIndex + GET_MSG_PREFIX.length); 265 | if (endIndex < 0) 266 | { 267 | return expFormat("State not loaded. Last state has a bad format."); 268 | } 269 | 270 | // Get the data-String from the content. 271 | const stateString = content.slice(getMsgIndex + GET_MSG_PREFIX.length, endIndex); 272 | 273 | // Load the data-string into the state. 274 | return expand("state set " + stateString); 275 | ``` 276 | __ 277 | state restore - Loads the session state from the last "state get" shortcut expansion in the current note. 278 | 279 | 280 | __ 281 | ``` 282 | ^state set (.+)$ 283 | ``` 284 | __ 285 | ```js 286 | // Try to parse the given data-string 287 | try 288 | { 289 | // If data-string parsed, assign to the state 290 | _inlineScripts.state.sessionState = JSON.parse($1); 291 | } 292 | catch (e) 293 | { 294 | // Notify the user of the failure to parse 295 | return expFormat([ "", "State not loaded. Invalid state:\n" + $1 ]); 296 | } 297 | 298 | // Call event callbacks for the state.onLoad event 299 | _inlineScripts.inlineScripts.HelperFncs.callEventListenerCollection( 300 | "state.onLoad", _inlineScripts.state.listeners.onLoad); 301 | 302 | return expFormat([ State set." ]); 303 | ``` 304 | __ 305 | state set {state: text} - Loads the session state from {state}, a state-string created with the "state get" shortcut. 306 | 307 | 308 | __ 309 | ``` 310 | ^state save$ 311 | ``` 312 | __ 313 | ```js 314 | // Get the current state's data-string. 315 | const stateString = JSON.stringify(_inlineScripts.state.sessionState); 316 | 317 | // If the data-string hasn't changed, do nothing 318 | if (stateString === _inlineScripts.state.stateFile.priorString) 319 | { 320 | return expFormat("State unchanged since prior save"); 321 | } 322 | 323 | // Write the data-string to the state data-file 324 | await app.vault.adapter.write( 325 | _inlineScripts.state.stateFile.path, stateString); 326 | 327 | // Remember the new data-string to check for future changes 328 | _inlineScripts.state.stateFile.priorString = stateString; 329 | 330 | return expFormat("State saved."); 331 | ``` 332 | __ 333 | state save - Store the state to file. This is called automatically after each shortcut expansion that triggers a state change. 334 | -------------------------------------------------------------------------------- /notevars.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | This shortcut-file includes shortcuts to get and set note-variables. Note-variables are variables set in the YAML frontmatter. They can be used by many plugins, including DataView and TTRPG StatBlocks. 6 | 7 | This shortcut file can be used by other shortcut-files to let them read and manipulate data in notes for many uses, including TTRPG character sheets. 8 | 9 | This shortcut-file has a tutorial video available: 10 | - [Using the "notevars" shortcut-file to work with note variables](https://www.youtube.com/watch?v=EsV4WcMwhbA) (runtime 6:59) 11 | 12 | Uses __state.sfile__ shortcut-file (optional). 13 | It uses this to save & load the isMarkdownRefreshed flag. 14 | 15 | 16 | __ 17 | ``` 18 | ^sfile setup$ 19 | ``` 20 | __ 21 | ```js 22 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 23 | 24 | // Initialize state (just a flag) 25 | confirmObjectPath( 26 | "_inlineScripts.state.sessionState.notevars.isMarkdownRefreshed", true); 27 | 28 | // Event callback - state.onReset 29 | confirmObjectPath( 30 | "_inlineScripts.state.listeners.onReset.notevars", 31 | function() 32 | { 33 | expand("notevars reset noconfirm"); 34 | }); 35 | ``` 36 | __ 37 | Sets up this shortcut-file 38 | 39 | 40 | __ 41 | ``` 42 | ^sfile shutdown$ 43 | ``` 44 | __ 45 | ```js 46 | // Event callback removal 47 | delete _inlineScripts.state?.listeners?.onReset?.notevars; 48 | 49 | // State removal 50 | delete _inlineScripts.state?.sessionState?.notevars; 51 | ``` 52 | __ 53 | Shuts down this shortcut-file 54 | 55 | 56 | __ 57 | ``` 58 | ^notevars reset$ 59 | ``` 60 | __ 61 | ```js 62 | // Confirm 63 | if (!popups.confirm("Confirm resetting the Notevars system")) { return null; } 64 | 65 | // Reset 66 | expand("notevars reset noconfirm"); 67 | 68 | return expFormat("Notevars cleared."); 69 | ``` 70 | __ 71 | notevars reset - Clears the isMarkdownRefreshed flag. 72 | *** 73 | 74 | 75 | __ 76 | ``` 77 | ^notevars reset noconfirm$ 78 | ``` 79 | __ 80 | ```js 81 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 82 | 83 | // Reset the state 84 | confirmObjectPath("_inlineScripts.state.sessionState.notevars"); 85 | _inlineScripts.state.sessionState.notevars.isMarkdownRefreshed = true; 86 | ``` 87 | __ 88 | hidden - No-confirm reset 89 | 90 | 91 | __ 92 | ``` 93 | ^notevars isMarkdownRefreshed ?(|[y|n])$ 94 | ``` 95 | __ 96 | ```js 97 | // If parameter is specified, update the flag based on it 98 | if ($1) 99 | { 100 | _inlineScripts.state.sessionState.notevars.isMarkdownRefreshed = ($1 === "y"); 101 | } 102 | 103 | // Return a notification announcing the flag's current state 104 | return expFormat( 105 | "notevars isMarkdownRefreshed is " + 106 | (_inlineScripts.state.sessionState.notevars.isMarkdownRefreshed ? 107 | "__enabled__" : "__disabled__")); 108 | ``` 109 | __ 110 | notevars isMarkdownRefreshed {state: y OR n, default: ""} - If {state} is given, assigns it to the notevars "isMarkdownRefreshed" flag. Otherwise, displays the current "isMarkdownRefreshed" flag. 111 | - If isMarkdownRefreshed flag is set then a note's markdown is refreshed each time one of it's variables is set. 112 | *** 113 | 114 | 115 | __ 116 | ``` 117 | ^notevars get ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ("[^ \t\['`\|\{\}">*&#@!].*"|[^ \t\['`\|\{\}">*&#@!][^ \t]*)$ 118 | ``` 119 | __ 120 | ```js 121 | // Remove any quotes around parameters 122 | $1 = $1.replace(/^"(.*)"$/, "$1") 123 | $2 = $2.replace(/^"(.*)"$/, "$1") 124 | 125 | // If notename is ".", change it to the current file 126 | if ($1 === ".") { $1 = app.workspace.getActiveFile()?.path; } 127 | 128 | // Get the file object for the specified note. Early if not available or is a folder 129 | const file = app.vault.fileMap[$1] || app.vault.fileMap[$1 + ".md"]; 130 | if (!file) 131 | { 132 | return expFormat([ "", "No value. Note __" + $1 + "__ not found." ]); 133 | } 134 | if (file.children) 135 | { 136 | return expFormat( 137 | [ "", "No value. __" + $1 + "__ is a folder." ]); 138 | } 139 | 140 | // Get the file's cached data. Early out if not available. 141 | const cache = app.metadataCache.getFileCache(file); 142 | if (!cache) 143 | { 144 | return expFormat( 145 | [ "", "No value. Note __" + $1 + "__ is not properly cached by Obsidian." ]); 146 | } 147 | 148 | // Get the front-matter object 149 | // If no front-matter, early out (no message, since it technically worked, but the 150 | // variable is empty) 151 | const fm = cache.frontmatter; 152 | if (!fm) 153 | { 154 | return null; 155 | } 156 | 157 | // Get the variable valuable 158 | const result = fm[$2] || fm[$2 + ":"] || null; 159 | 160 | // Return the result. If it's an array, it's joined to a single string. 161 | return Array.isArray(result) ? result.join(",") : result; 162 | ``` 163 | __ 164 | notevars get {note name: path text} {variable name: yaml name text} - Expands to the value of variable {variable name} in note {note name}. If {note name} is "." then it represents the current note. 165 | 166 | 167 | __ 168 | ``` 169 | ^notevars getArray ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ("[^ \t\['`\|\{\}">*&#@!].*"|[^ \t\['`\|\{\}">*&#@!][^ \t]*) ([0-9]+)$ 170 | ``` 171 | __ 172 | ```js 173 | // Remove any quotes around parameters 174 | $1 = $1.replace(/^"(.*)"$/, "$1") 175 | $2 = $2.replace(/^"(.*)"$/, "$1") 176 | 177 | // If notename is ".", change it to the current file 178 | if ($1 === ".") { $1 = app.workspace.getActiveFile()?.path; } 179 | 180 | // Get the file object for the specified note. Early out if unavailable, or folder 181 | const file = app.vault.fileMap[$1] || app.vault.fileMap[$1 + ".md"]; 182 | if (!file) 183 | { 184 | return expFormat([ "", "No value. Note __" + $1 + "__ not found." ]); 185 | } 186 | if (file.children) 187 | { 188 | return expFormat( 189 | [ "", "No value. __" + $1 + "__ is a folder." ]); 190 | } 191 | 192 | // Get the file's cached data. Early out if unavailable 193 | const cache = app.metadataCache.getFileCache(file); 194 | if (!cache) 195 | { 196 | return expFormat( 197 | [ "", "No value. Note __" + $1 + "__ is not properly cached by " + 198 | "Obsidian." ]); 199 | } 200 | 201 | // Get the front-matter object 202 | // If no front-matter, early out (no message, since it technically worked, but the 203 | // variable is empty) 204 | const fm = cache.frontmatter; 205 | if (!fm) 206 | { 207 | return null; 208 | } 209 | 210 | // Get the variable valuable 211 | const result = fm[$2] || fm[$2 + ":"] || null; 212 | 213 | // Return the result, indexed by $3 (as an array) 214 | return Array.isArray(result) ? result[$3] : ($3 === "0") ? result : null; 215 | ``` 216 | __ 217 | notevars getArray {note name: path text} {array name: yaml name text} {index: >=0} - Expands to the value of item {index} of array {array name} in note {note name}. If {note name} is "." then it represents the current note. 218 | 219 | 220 | __ 221 | ``` 222 | ^notevars set ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ("[^ \t\['`\|\{\}">*&#@!].*"|[^ \t\['`\|\{\}">*&#@!][^ \t]*) (.*)$ 223 | ``` 224 | __ 225 | ```js 226 | // Remove any quotes around parameters 227 | $1 = $1.replace(/^"(.*)"$/, "$1") 228 | $2 = $2.replace(/^"(.*)"$/, "$1") 229 | 230 | // If notename is ".", change it to the current file 231 | if ($1 === ".") { $1 = app.workspace.getActiveFile()?.path || ""; } 232 | 233 | // Get the file object for the specified note. Early out if unavailable or is folder 234 | const file = app.vault.fileMap[$1] || app.vault.fileMap[$1 + ".md"]; 235 | if (!file) 236 | { 237 | return expFormat( 238 | [ "", "Variable __" + $2 + "__ not set. Note __" + $1 + "__ not found." ]); 239 | } 240 | if (file.children) 241 | { 242 | return expFormat( 243 | [ "", "Variable __" + $2 + "__ not set. __" + $1 + "__ is a folder." ]); 244 | } 245 | 246 | // Get the file's content. Early out if unavailable. 247 | const content = await app.vault.cachedRead(file); 248 | if (content === null || content === undefined) 249 | { 250 | return expFormat( 251 | [ "", "Variable __" + $2 + "__ not set. Note __" + $1 + "__ read failed." ]); 252 | } 253 | 254 | // Adjust value to resolve escape characters for newline and tab 255 | $3 = $3.trim().replaceAll("\\n", "\n").replaceAll("\\t", "\t"); 256 | 257 | // Get the file content's frontmatter 258 | const fmMatch = content.match(/^(\n*---)(\n[\S\s]*\n)(\n*---\n)/); 259 | 260 | // Is the new value multiline? 261 | const isValueMultiline = $3.includes("\n") || $3[0] === "-"; 262 | 263 | // Determine what to add and where 264 | let toAdd = { text: "", start: 0, end: 0 }; 265 | 266 | // If there isn't yet a frontmatter, create it along with the variable name/value pair 267 | if (!fmMatch) 268 | { 269 | // Define what to add 270 | toAdd.text = "---\n" + $2 + ": " + $3 + "\n---\n\n"; 271 | } 272 | 273 | // If there IS a frontmatter, add the variable name/value pair to it 274 | else 275 | { 276 | // Find the variable in the frontmatter 277 | const varMatch = 278 | fmMatch[2].match("\n( *)(" + $2 + " *:)(\n|(?: |\t)[^\n]*\n)"); 279 | 280 | // If the variable is NOT found append it to the frontmatter 281 | if (!varMatch) 282 | { 283 | toAdd.text = $2 + ( 284 | isValueMultiline ? 285 | ":\n " + $3.replaceAll("\n", "\n ") + "\n" : 286 | ": " + $3 + "\n" 287 | ); 288 | toAdd.start = fmMatch[1].length + fmMatch[2].length; 289 | toAdd.end = toAdd.start; 290 | } 291 | 292 | // If the variable IS found, modify it 293 | else 294 | { 295 | // Define value-entry 296 | const lineIndent = "\n" + varMatch[1] + " "; 297 | toAdd.text = 298 | isValueMultiline ? 299 | lineIndent + $3.replaceAll("\n", lineIndent) : 300 | " " + $3; 301 | // Define start of definition 302 | toAdd.start = 303 | fmMatch[1].length + varMatch.index + varMatch[1].length + 304 | varMatch[2].length + 1; 305 | // Define end of definition 306 | let lineStart = toAdd.start; 307 | let lineEnd = content.indexOf("\n", toAdd.start + 1); 308 | if (varMatch[3].trim()) 309 | { 310 | // Old definition is single-line 311 | toAdd.end = lineEnd; 312 | } 313 | else 314 | { 315 | // Old definition is multi-line 316 | let lineStartRegex = new RegExp("^" + varMatch[1] + "(?:- | )"); 317 | while (lineEnd !== -1) 318 | { 319 | var line = content.slice(lineStart + 1, lineEnd); 320 | if (!line.match(lineStartRegex)) 321 | { 322 | toAdd.end = lineStart; 323 | break; 324 | } 325 | lineStart = lineEnd; 326 | lineEnd = content.indexOf("\n", lineStart + 1); 327 | } 328 | } 329 | } 330 | } 331 | 332 | // Force the reading views to update (dataview isn't currently working for this) 333 | if (_inlineScripts.state.sessionState.notevars.isMarkdownRefreshed) 334 | { 335 | const leaves = _inlineScripts.inlineScripts.HelperFncs.getLeavesForFile(file); 336 | if (leaves.length) 337 | { 338 | const onChanged = (changedFile) => 339 | { 340 | // If the changed file is the one we're monitoring 341 | if (changedFile === file) 342 | { 343 | // Stop monitoring for file changes 344 | app.metadataCache.off("changed", onChanged); 345 | 346 | // Force all views of the file to re-render on the next frame 347 | setTimeout(() => 348 | { 349 | for (const leaf of leaves) 350 | { 351 | leaf.view.modes.preview.rerender(true); 352 | } 353 | }, 100); 354 | } 355 | } 356 | app.metadataCache.on("changed", onChanged); 357 | } 358 | } 359 | 360 | // Modify the variable in the frontmatter of the note 361 | await _inlineScripts.inlineScripts.HelperFncs.addToNote(toAdd.text, toAdd, file.path); 362 | 363 | return expFormat( 364 | "Note __" + $1 + "__, variable __" + $2 + "__ set to __" + $3 + "__."); 365 | ``` 366 | __ 367 | notevars set {note name: path text} {variable name: yaml name text} {value: text} - Sets the value of variable {variable name} to {value} in note {note name}. If {note name} is "." then it represents the current note. 368 | 369 | 370 | __ 371 | ``` 372 | ^notevars setArray ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+) ("[^ \t\['`\|\{\}">*&#@!].*"|[^ \t\['`\|\{\}">*&#@!][^ \t]*) (.*)$ 373 | ``` 374 | __ 375 | ```js 376 | // Remove any quotes around parameters 377 | $1 = $1.replace(/^"(.*)"$/, "$1") 378 | $2 = $2.replace(/^"(.*)"$/, "$1") 379 | 380 | // Get the input array 381 | const valueArray = $3.split(",").map(v => v.trim()); 382 | 383 | // Call "notevars set" with the input array 384 | const result = 385 | expand("notevars set " + $1 + " " + $2 + " " + "- " + valueArray.join("\\n- ")); 386 | 387 | // If error with "notevars set", return that error 388 | if (!result[0]) { return result; } 389 | 390 | return expFormat( 391 | "Note __" + $1 + "__, variable __" + $2 + "__ set to array __\\[ " + 392 | valueArray.join(", ") + " \\]__."); 393 | ``` 394 | __ 395 | notevars setArray {note name: path text} {array name: yaml name text} {values: text (comma separated)} - Sets the values of array {array name} to {value1}, {value2}, etc. in note {note name}. If {note name} is "." then it represents the current note. 396 | -------------------------------------------------------------------------------- /une.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts for UNE: The Universal NPC Emulator. UNE is an excellent character generation system for tabletop role playing and general storytelling. It was designed by Zach Best. 6 | You can find more info about UNE at its [drivethrurpg page](https://www.drivethrurpg.com/product/134163/UNE-The-Universal-NPC-Emulator-rev). 7 | 8 | This shortcut-file has supplementary shortcut files: 9 | __une_ui.sfile__ - Graphical UI versions of some of these shortcuts. 10 | 11 | 12 | __ 13 | __ 14 | ```js 15 | // Make a roll from 1 to max. 16 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 17 | 18 | // Pick an item from array a. 19 | function aPick(a) { return a[roll(a.length)-1]; } 20 | 21 | // Pick an item from array a, weighted by element wIndex of the item. If theRoll is passed, use that as the roll. 22 | function aPickWeight(a, wIndex, theRoll) 23 | { 24 | wIndex = wIndex || 1; 25 | theRoll = theRoll || roll(a.last()[wIndex]); 26 | for (const item of a) 27 | { 28 | if (item[wIndex] >= theRoll) 29 | { 30 | return item; 31 | } 32 | } 33 | return a.last(); 34 | } 35 | ``` 36 | __ 37 | Helper scripts 38 | 39 | 40 | __ 41 | ``` 42 | ^une ?(|[1-5]) ?(|[1-7]) ?(|[1-8])$ 43 | ``` 44 | __ 45 | ```js 46 | // Combine 2 combo-results: 47 | // - each needs separate formatting, since separator mustn't have formatting 48 | // - The first needs the footer dropped, since it's not at the end anymore 49 | // - The second needs the header dropped, since it's not at the start anymore 50 | return "" + 51 | expFormat(expUnformat(expand("une character " + $1)), false, false, true) + 52 | "\n***\n" + 53 | expFormat(expUnformat(expand("une interact " + $2 + " " + $3)), true); 54 | ``` 55 | __ 56 | une {randomness: 1 TO 5 (order to chaos), default: 3 (standard)} {relationship to PC: 1 TO 7 (love to hate), default: 4 (neutral)} {demeanor: 1 TO 8 (scheming to prejudiced), default: random} - Runs "une character" and "une interact" together. {randomness} is a value for "une character". {relationship to PC} and {demeanor} are values for "une interact". 57 | *** 58 | 59 | 60 | __ 61 | ``` 62 | ^une character ?(|[1-5])$ 63 | ``` 64 | __ 65 | ```js 66 | // Combine 3 results: 67 | // - remove their exp formatting and line-separated titles 68 | // - add new exp formatting and inline titles (except for motives) 69 | return expFormat("Character:" + 70 | "\nidentity: " + expUnformat(expand("une identity"))[1] + 71 | "\npower: " + expUnformat(expand("une power " + $1))[1] + 72 | "\nmotives:\n" + expUnformat(expand("une motive"))[1] 73 | ); 74 | ``` 75 | __ 76 | une character {randomness: 1 TO 5 (order to chaos), default: 3 (standard)} - Runs "identity", "power" and "motive" together. {randomness} is a value for "power". 77 | 78 | 79 | __ 80 | ``` 81 | ^une interact ?(|[1-7]) ?(|[1-8])$ 82 | ``` 83 | __ 84 | ```js 85 | // Combine 3 results: 86 | // - remove their exp formatting and line-separated titles 87 | // - add new exp formatting and inline titles 88 | return expFormat("Interact:" + 89 | "\nmood: " + expUnformat(expand("une mood " + $1))[1] + 90 | "\nbearing: " + expUnformat(expand("une bearing " + $2))[1] + 91 | "\nfocus: " + expUnformat(expand("une focus"))[1] 92 | ); 93 | ``` 94 | __ 95 | une interact {relationship to PC: 1 TO 7 (love to hate), default: 4 (neutral)} {demeanor: 1 TO 8 (scheming to prejudiced), default: random} - Runs "mood", "bearing" and "focus" together. {relationship to PC} is a value for "mood". {demeanor} is a value for "bearing". 96 | *** 97 | 98 | 99 | __ 100 | ``` 101 | ^une identity$ 102 | ``` 103 | __ 104 | ```js 105 | // Data 106 | const table1 = 107 | ["SUPERFLUOUS","ADDICTED","CONFORMIST","NEFARIOUS","SENSIBLE","UNTRAINED","ROMANTIC","UNREASONABLE","SKILLED","NEGLECTFUL","LIVELY","FORTHRIGHT","IDEALISTIC","UNSUPPORTIVE","RATIONAL","COARSE","FOOLISH","CUNNING","DELIGHTFUL","MISERLY","INEPT","BANAL","LOGICAL","SUBTLE","REPUTABLE","WICKED","LAZY","PESSIMISTIC","SOLEMN","HABITUAL","MEEK","HELPFUL","UNCONCERNED","GENEROUS","DOCILE","CHEERY","PRAGMATIC","SERENE","THOUGHTFUL","HOPELESS","PLEASANT","INSENSITIVE","TITLED","INEXPERIENCED","PRYING","OBLIVIOUS","REFINED","INDISPENSABLE","SCHOLARLY","CONSERVATIVE","UNCOUTH","WILLFUL","INDIFFERENT","FICKLE","ELDERLY","SINFUL","NAIVE","PRIVILEGED","GLUM","LIKABLE","LETHARGIC","DEFIANT","OBNOXIOUS","INSIGHTFUL","TACTLESS","FANATIC","PLEBEIAN","CHILDISH","PIOUS","UNEDUCATED","INCONSIDERATE","CULTURED","REVOLTING","CURIOUS","TOUCHY","NEEDY","DIGNIFIED","PUSHY","KIND","CORRUPT","JOVIAL","SHREWD","LIBERAL","COMPLIANT","DESTITUTE","CONNIVING","CAREFUL","ALLURING","DEFECTIVE","OPTIMISTIC","AFFLUENT","DESPONDENT","MINDLESS","PASSIONATE","DEVOTED","ESTABLISHED","UNSEEMLY","DEPENDABLE","RIGHTEOUS","CONFIDENT"]; 108 | const table2 = 109 | ["GYPSY","WITCH","MERCHANT","EXPERT","COMMONER","JUDGE","RANGER","OCCULTIST","REVEREND","THUG","DRIFTER","JOURNEYMAN","STATESMAN","ASTROLOGER","DUELIST","JACK-OF-ALL-TRADES","ARISTOCRAT","PREACHER","ARTISAN","ROGUE","MISSIONARY","OUTCAST","MERCENARY","CARETAKER","HERMIT","ORATOR","CHIEFTAIN","PIONEER","BURGLAR","VICAR","OFFICER","EXPLORER","WARDEN","OUTLAW","ADEPT","BUM","SORCERER","LABORER","MASTER","ASCENDANT","VILLAGER","MAGUS","CONSCRIPT","WORKER","ACTOR","HERALD","HIGHWAYMAN","FORTUNE-HUNTER","GOVERNOR","SCRAPPER","MONK","HOMEMAKER","RECLUSE","STEWARD","POLYMATH","MAGICIAN","TRAVELER","VAGRANT","APPRENTICE","POLITICIAN","MEDIATOR","CROOK","CIVILIAN","ACTIVIST","HERO","CHAMPION","CLERIC","SLAVE","GUNMAN","CLAIRVOYANT","PATRIARCH","SHOPKEEPER","CRONE","ADVENTURER","SOLDIER","ENTERTAINER","CRAFTSMAN","SCIENTIST","ASCETIC","SUPERIOR","PERFORMER","MAGISTER","SERF","BRUTE","INQUISITOR","LORD","VILLAIN","PROFESSOR","SERVANT","CHARMER","GLOBETROTTER","SNIPER","COURTIER","PRIEST","TRADESMAN","HITMAN","WIZARD","BEGGAR","TRADESMAN","WARRIOR"]; 110 | 111 | // Simply combine a roll of table 1 and a roll of table 2 112 | return expFormat([ "NPC identity:\n", aPick(table1) + " " + aPick(table2) ]); 113 | ``` 114 | __ 115 | une identity - Generates a 2-word description for an NPC. 116 | 117 | 118 | __ 119 | ``` 120 | ^une power ?(|[1-5])$ 121 | ``` 122 | __ 123 | ```js 124 | // The parameter defaults to 3 125 | $1 = Number($1) || 3; 126 | 127 | // Data 128 | const table3 = 129 | [ ["MUCH WEAKER",2,4,5,8,12],["SLIGHTLY WEAKER",10,15,20,25,30],["COMPARABLE",90,85,80,75,70],["SLIGHTLY STRONGER",98,96,95,92,88],["MUCH STRONGER",100,100,100,100,100] ]; 130 | 131 | // Result is a weighted roll of table3, which weight being defined by the parameter 132 | return expFormat([ "NPC power level:\n", aPickWeight(table3, $1)[0] ]); 133 | ``` 134 | __ 135 | une power {randomness: 1 TO 5 (order to chaos), default: 3 (standard)} - Generates an NPC's power level relative to PC's power level, based on {randomness}. 136 | 137 | 138 | __ 139 | ``` 140 | ^une motive$ 141 | ``` 142 | __ 143 | ```js 144 | // Data 145 | const table4 = 146 | ["ADVISE","OBTAIN","ATTEMPT","SPOIL","OPPRESS","INTERACT","CREATE","ABDUCT","PROMOTE","CONCEIVE","BLIGHT","PROGRESS","DISTRESS","POSSESS","RECORD","EMBRACE","CONTACT","PURSUE","ASSOCIATE","PREPARE","SHEPHERD","ABUSE","INDULGE","CHRONICLE","FULFILL","DRIVE","REVIEW","AID","FOLLOW","ADVANCE","GUARD","CONQUER","HINDER","PLUNDER","CONSTRUCT","ENCOURAGE","AGONIZE","COMPREHEND","ADMINISTER","RELATE","TAKE","DISCOVER","DETER","ACQUIRE","DAMAGE","PUBLICIZE","BURDEN","ADVOCATE","IMPLEMENT","UNDERSTAND","COLLABORATE","STRIVE","COMPLETE","COMPEL","JOIN","ASSIST","DEFILE","PRODUCE","INSTITUTE","ACCOUNT","WORK","ACCOMPANY","OFFEND","GUIDE","LEARN","PERSECUTE","COMMUNICATE","PROCESS","REPORT","DEVELOP","STEAL","SUGGEST","WEAKEN","ACHIEVE","SECURE","INFORM","PATRONIZE","DEPRESS","DETERMINE","SEEK","MANAGE","SUPPRESS","PROCLAIM","OPERATE","ACCESS","REFINE","COMPOSE","UNDERMINE","EXPLAIN","DISCOURAGE","ATTEND","DETECT","EXECUTE","MAINTAIN","REALIZE","CONVEY","ROB","ESTABLISH","OVERTHROW","SUPPORT"]; 147 | const table5 = 148 | ["WEALTH","HARDSHIP","AFFLUENCE","RESOURCES","PROSPERITY","POVERTY","OPULENCE","DEPRIVATION","SUCCESS","DISTRESS","CONTRABAND","MUSIC","LITERATURE","TECHNOLOGY","ALCOHOL","MEDICINES","BEAUTY","STRENGTH","INTELLIGENCE","FORCE","THE_WEALTHY","THE_POPULOUS","ENEMIES","THE_PUBLIC","RELIGION","THE_POOR","FAMILY","THE_ELITE","ACADEMIA","THE_FORSAKEN","THE_LAW","THE_GOVERNMENT","THE_OPPRESSED","FRIENDS","CRIMINALS","ALLIES","SECRET_SOCIETIES","THE_WORLD","MILITARY","THE_CHURCH","DREAMS","DISCRETION","LOVE","FREEDOM","PAIN","FAITH","SLAVERY","ENLIGHTENMENT","RACISM","SENSUALITY","DISSONANCE","PEACE","DISCRIMINATION","DISBELIEF","PLEASURE","HATE","HAPPINESS","SERVITUDE","HARMONY","JUSTICE","GLUTTONY","LUST","ENVY","GREED","LAZINESS","WRATH","PRIDE","PURITY","MODERATION","VIGILANCE","ZEAL","COMPOSURE","CHARITY","MODESTY","ATROCITIES","COWARDICE","NARCISSISM","COMPASSION","VALOR","PATIENCE","ADVICE","PROPAGANDA","SCIENCE","KNOWLEDGE","COMMUNICATIONS","LIES","MYTHS","RIDDLES","STORIES","LEGENDS","INDUSTRY","NEW_RELIGIONS","PROGRESS","ANIMALS","GHOSTS","MAGIC","NATURE","OLD_RELIGIONS","EXPERTISE","SPIRITS"]; 149 | 150 | // Helper function - Get the table column of a table index 151 | function tableColumn(index) { return Math.trunc(index / 20); } 152 | 153 | // Result is three motives 154 | // a motive is: (a) a roll on table1 plus (b) a roll on table2 with unique column 155 | let result = []; 156 | 157 | // Track noun rolls to make sure they all have unique columns 158 | let priorNounRolls = []; 159 | 160 | // Roll the three motives 161 | for (let i = 0; i < 3; i++) 162 | { 163 | // Fill this with a noun roll with a unique column 164 | let nounRoll; 165 | 166 | // Find a noun roll with a unique column 167 | do 168 | { 169 | // Pick a random noun roll 170 | nounRoll = roll(100); 171 | 172 | // Check if the noun roll shares a previous noun roll's column 173 | let hasAUniqueColumn = true; 174 | for (let i = 0; i < priorNounRolls.length; i++) 175 | { 176 | if (tableColumn(priorNounRolls[i] - 1) === tableColumn(nounRoll - 1)) 177 | { 178 | hasAUniqueColumn = false; 179 | break; 180 | } 181 | } 182 | 183 | // If we found a noun roll with a unique column, stop searching 184 | if (hasAUniqueColumn) { break; } 185 | } 186 | while (true); 187 | 188 | // Add a new motive (with the uniquely columned noun roll) to the list of motives 189 | result.push(aPick(table4) + " " + table5[nounRoll-1]); 190 | 191 | // Add the noun roll to the list of prior nouns rolls 192 | priorNounRolls.push(nounRoll); 193 | } 194 | 195 | return expFormat([ "NPC motives:\n", ". " + result.join("\n. ") ]); 196 | ``` 197 | __ 198 | une motive - Generates three 2-word descriptions for an NPC's motivations. 199 | *** 200 | 201 | 202 | __ 203 | ``` 204 | ^une mood ?(|[1-7])$ 205 | ``` 206 | __ 207 | ```js 208 | // Parameter defaults to 4 209 | $1 = Number($1) || 4; 210 | 211 | // Data 212 | const table6 = [ 213 | ["WITHDRAWN",1,2,3,5,7,11,15], 214 | ["GUARDED",6,8,11,15,18,24,30], 215 | ["CAUTIOUS",16,20,25,30,46,61,69], 216 | ["NEUTRAL",31,40,55,60,76,81,84], 217 | ["SOCIABLE",70,76,82,85,90,93,94], 218 | ["HELPFUL",85,89,93,95,97,98,99], 219 | ["FORTHCOMING",100,100,100,100,100,100,100] ]; 220 | 221 | // Result is a weighted roll of table6, which weight being defined by the parameter 222 | return expFormat([ "NPC mood:\n", aPickWeight(table6, $1)[0] ]); 223 | ``` 224 | __ 225 | une mood {relationship to PC: 1 TO 7 (love to hate), default: 4 (neutral)} - Generates an NPC's willingness to socialize for this interaction, based on {relationship to PC}. 226 | 227 | 228 | __ 229 | ``` 230 | ^une bearing ?(|[1-8])$ 231 | ``` 232 | __ 233 | ```js 234 | // Parameter defaults to a random number from 1 to 8 235 | $1 = Number($1) || roll(8) 236 | 237 | // Data 238 | const table7a = ["SCHEMING", "INSANE", "FRIENDLY", "HOSTILE", "INQUISITIVE", "KNOWING", "MYSTERIOUS", "PREJUDICED"]; 239 | const table7b = [ 240 | ["INTENT","BARGAIN","MEANS","PROPOSITION","PLAN","COMPROMISE","AGENDA","ARRANGEMENT","NEGOTIATION","PLOT"], 241 | ["MADNESS","FEAR","ACCIDENT","CHAOS","IDIOCY","ILLUSION","TURMOIL","CONFUSION","FACADE","BEWILDERMENT"], 242 | ["ALLIANCE","COMFORT","GRATITUDE","SHELTER","HAPPINESS","SUPPORT","PROMISE","DELIGHT","AID","CELEBRATION"], 243 | ["DEATH","CAPTURE","JUDGMENT","COMBAT","SURRENDER","RAGE","RESENTMENT","SUBMISSION","INJURY","DESTRUCTION"], 244 | ["QUESTIONS","INVESTIGATION","INTEREST","DEMAND","SUSPICION","REQUEST","CURIOSITY","SKEPTICISM","COMMAND","PETITION"], 245 | ["REPORT","EFFECTS","EXAMINATION","RECORDS","ACCOUNT","NEWS","HISTORY","TELLING","DISCOURSE","SPEECH"], 246 | ["RUMOR","UNCERTAINTY","SECRETS","MISDIRECTION","WHISPERS","LIES","SHADOWS","ENIGMA","OBSCURITY","CONUNDRUM"], 247 | ["REPUTATION","DOUBT","BIAS","DISLIKE","PARTIALITY","BELIEF","VIEW","DISCRIMINATION","ASSESSMENT","DIFFERENCE"] ]; 248 | 249 | // Result is based on demeanor and bearing. Demeanor is parameter 1, bearing is a roll 250 | let demeanor = $1 251 | let bearing = roll(10); 252 | 253 | // Result is an element from the 2-d table 7b, chosen by demeanor and bearing. 254 | // Also show the demeanor as a text from table7a 255 | let result = table7a[demeanor-1] + " - " + table7b[demeanor-1][bearing-1]; 256 | 257 | return expFormat([ "NPC bearing:\n", result ]); 258 | ``` 259 | __ 260 | une bearing {demeanor: 1 TO 8 (scheming to prejudiced), default: random} - Generates an NPC's attitude for this interaction, based on {demeanor}: a number defaulting to random and meaning one of the following: 261 | 1 - sceming 2 - insane 3 - friendly 4 - hostile 262 | 5 - inquisitive 6 - knowing 7 - mysterious 8 - prejudiced 263 | 264 | 265 | __ 266 | ``` 267 | ^une focus$ 268 | ``` 269 | __ 270 | ```js 271 | // Data 272 | const table8 = [ ["CURRENT_SCENE", 3],["LAST_STORY",6],["EQUIPMENT",9],["PARENTS",12],["HISTORY",15],["RETAINERS",18],["WEALTH",21],["RELICS",24],["LAST_ACTION",27],["SKILLS",30],["SUPERIORS",33],["FAME",36],["CAMPAIGN",39],["FUTURE_ACTION",42],["FRIENDS",45],["ALLIES",48],["LAST_SCENE",51],["CONTACTS",54],["FLAWS",57],["ANTAGONIST",60],["REWARDS",63],["EXPERIENCE",66],["KNOWLEDGE",69],["RECENT_SCENE",72],["COMMUNITY",75],["TREASURE",78],["THE_CHARACTER",81],["CURRENT_STORY",84],["FAMILY",87],["POWER",90],["WEAPONS",93],["PREVIOUS_SCENE",96],["ENEMY",100] ]; 273 | 274 | // Result is a simple weighted roll 275 | let result = "THE PC'S " + aPickWeight(table8)[0]; 276 | 277 | return expFormat([ "NPC focus:\n", result ]); 278 | ``` 279 | __ 280 | une focus - Generates an NPC's primary interest for this interaction. 281 | -------------------------------------------------------------------------------- /cards_pileviewer.sfile.md: -------------------------------------------------------------------------------- 1 | Adds an Obsidian panel type that lets you view card-piles. 2 | 3 | This shortcut-file has a tutorial video available: 4 | [Using the "cards" shortcut-file to use virtual cards](https://www.youtube.com/watch?v=KkpjTL2UvtQ) (runtime 12:43) 5 | 6 | 7 | __ 8 | __ 9 | ```js 10 | // This is referred to by a few different systems 11 | const CARDPILE_VIEW_TYPE = "inline-scripts-cardpile-view"; 12 | 13 | // Get the current back-image, url 14 | function getBackImage() 15 | { 16 | return _inlineScripts.state.sessionState.cards.backImage || 17 | _inlineScripts.cards.defaultBackImage; 18 | } 19 | 20 | // Turn a relative path url into an absolute path url based in the vault's root 21 | function getAbsolutePath(path) 22 | { 23 | if (path.startsWith("data:image")) { return path; } 24 | path = app.vault.fileMap[path]; 25 | if (!path) { return ""; } 26 | return app.vault.getResourcePath(path); 27 | } 28 | 29 | // Create a block of html to represent a specific card 30 | function createCardUi(isFaceUp, card, id, scale, includeDataSrc) 31 | { 32 | const result = document.createElement("img"); 33 | result.classList.add("cardUi"); 34 | result.src = getAbsolutePath(isFaceUp ? card.path : getBackImage()); 35 | const size = _inlineScripts.state.sessionState.cards.size * (scale || 1.0); 36 | result.style.width = size + "px"; 37 | result.style.height = (size * card.aspect) + "px"; 38 | result.dataset.id = id; 39 | if (includeDataSrc) 40 | { 41 | result.dataset.src = getAbsolutePath(card.path); 42 | } 43 | if (isFaceUp) 44 | { 45 | switch (card.rotation) 46 | { 47 | case 1: result.classList.add("rotated1"); break; 48 | case 2: result.classList.add("rotated2"); break; 49 | case 3: result.classList.add("rotated3"); break; 50 | } 51 | } 52 | return result; 53 | } 54 | ``` 55 | __ 56 | Helper scripts 57 | 58 | 59 | __ 60 | ``` 61 | ^sfile setup$ 62 | ``` 63 | __ 64 | ```js 65 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 66 | 67 | // Event callbacks for cards system events 68 | confirmObjectPath( 69 | "_inlineScripts.cards.listeners.onPileListChanged.cards_pileViewer", 70 | function() 71 | { 72 | // React to pile list changes by updating the dropdown list 73 | const leaves = app.workspace.getLeavesOfType(CARDPILE_VIEW_TYPE); 74 | for (const leaf of leaves) 75 | { 76 | leaf.view.refreshPileList(); 77 | } 78 | }); 79 | confirmObjectPath( 80 | "_inlineScripts.cards.listeners.onPileChanged.cards_pileViewer", 81 | function() 82 | { 83 | // React to pile changes by refreshing the current pile of every view 84 | const leaves = app.workspace.getLeavesOfType(CARDPILE_VIEW_TYPE); 85 | for (const leaf of leaves) 86 | { 87 | leaf.view.refreshPile(); 88 | } 89 | }); 90 | 91 | // Custom CSS 92 | _inlineScripts.inlineScripts.HelperFncs.addCss("cards_pileviewer", ".iscript_pileViewer_container { display: flex; flex-direction: column; padding: 0.5em !important; } .iscript_pileViewer_header { display: flex; margin-bottom: 0.5em; } .iscript_pileViewer_content { overflow-y: scroll; } .iscript_pileViewer_select { flex-grow: 1; margin-right: 0.25em; } .iscript_emptyMsg { text-align: center; margin-top: 1em; font-weight: bold; color: grey } .iscript_notDragged { filter: brightness(50%); } .iscript_viewerCardUi { margin: .1em; -webkit-user-drag: none; }"); 93 | 94 | // Panel icon 95 | _inlineScripts.inlineScripts.HelperFncs.addIcon(CARDPILE_VIEW_TYPE, ` 96 | 97 | 98 | Layer 1 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | `); 112 | 113 | // Only register this viewer panel registration once per Obsidian session 114 | if (_inlineScripts.inlineScripts.hasRegisteredCardPileView) { return; } 115 | _inlineScripts.inlineScripts.hasRegisteredCardPileView = true; 116 | 117 | // This viewer panel's class for registration 118 | class CardPileView extends _inlineScripts.inlineScripts.HelperFncs.ItemView 119 | { 120 | // Member vars 121 | pileSelect; 122 | zoomSelect; 123 | cardDisplay; 124 | dragReorder; 125 | _onDragReordered; 126 | 127 | constructor(leaf) 128 | { 129 | super(leaf); 130 | this._onDragReordered = this.onDragReordered.bind(this); 131 | } 132 | 133 | // Initial setup 134 | load() 135 | { 136 | // UI element creation 137 | const root = this.containerEl.children[1]; 138 | root.style.overflow = "unset"; 139 | root.classList.add("iscript_pileViewer_container"); 140 | // Row for the pile select and the zoom select 141 | const header = document.createElement('div'); 142 | header.classList.add("iscript_pileViewer_header"); 143 | root.appendChild(header); 144 | // The pile select for selecting the current pile 145 | this.pileSelect = document.createElement("select"); 146 | this.pileSelect.classList.add("iscript_pileViewer_select"); 147 | this.pileSelect.options[this.pileSelect.options.length] = new Option(""); 148 | this.pileSelect.onchange = () => { this.refreshPile(true); } 149 | header.appendChild(this.pileSelect); 150 | // The zoom select for selecting the current zoom 151 | this.zoomSelect = document.createElement("select"); 152 | this.zoomSelect.options[0] = new Option("25%"); 153 | this.zoomSelect.options[1] = new Option("50%"); 154 | this.zoomSelect.options[2] = new Option("75%"); 155 | this.zoomSelect.options[3] = new Option("100%", undefined, undefined, true); 156 | this.zoomSelect.options[4] = new Option("200%"); 157 | this.zoomSelect.options[5] = new Option("300%"); 158 | this.zoomSelect.onchange = () => 159 | { 160 | this.refreshPile(true); 161 | } 162 | header.appendChild(this.zoomSelect); 163 | // Main space for card visualization UIs 164 | this.cardDisplay = document.createElement("div"); 165 | this.cardDisplay.classList.add("iscript_pileViewer_content"); 166 | root.appendChild(this.cardDisplay); 167 | 168 | // First refresh of list of piles in the select ui 169 | this.refreshPileList(); 170 | } 171 | 172 | // Update the list of piles in the select ui 173 | refreshPileList() 174 | { 175 | // Remember the current pile while the select is being re-populated 176 | const oldValue = this.pileSelect.value; 177 | 178 | // Clear the select 179 | this.pileSelect.options.length = 0; 180 | 181 | // Add a blank option 182 | this.pileSelect.options[this.pileSelect.options.length] = new Option(""); 183 | 184 | // Add an option for each pile 185 | const piles = Object.keys(_inlineScripts.state.sessionState.cards.piles); 186 | piles.sort(); 187 | for (const pile of piles) 188 | { 189 | this.pileSelect.options[this.pileSelect.options.length] = 190 | new Option(pile); 191 | } 192 | 193 | // Restore the current pile. If not available, clear the pile view (refresh). 194 | this.pileSelect.value = oldValue; 195 | if (this.pileSelect.value !== oldValue) 196 | { 197 | this.refreshPile(true); 198 | } 199 | } 200 | 201 | // Update the visualization of the current pile being viewed 202 | refreshPile(force) 203 | { 204 | // Only refresh if being forced, or if the pile that changed is specified and 205 | // is the current pile 206 | if (!force && _inlineScripts.cards.listeners.changedPile && 207 | _inlineScripts.cards.listeners.changedPile !== this.pileSelect.value) 208 | { 209 | return; 210 | } 211 | 212 | // Clear the view to repopulate it 213 | this.cardDisplay.innerText = ""; 214 | 215 | // Get the current pile 216 | const pile = 217 | _inlineScripts.state.sessionState.cards.piles[this.pileSelect.value]; 218 | if (!pile) { return; } 219 | 220 | // If pile has no cards, add a ui saying so, then early out 221 | if (!pile.cards.length) 222 | { 223 | let emptyMsg = document.createElement("div"); 224 | emptyMsg.innerText = "-- Empty card-pile --"; 225 | emptyMsg.classList.add("iscript_emptyMsg"); 226 | this.cardDisplay.append(emptyMsg); 227 | return; 228 | } 229 | 230 | // Work out the zoom level 231 | const zoom = Number(this.zoomSelect.value.slice(0,-1)) / 100.0; 232 | 233 | // Add a visualization ui for each card in the current pile 234 | for (let i = pile.cards.length-1; i >= 0; i--) 235 | { 236 | let cardUi = createCardUi(pile.isFaceUp, pile.cards[i], i, zoom); 237 | cardUi.classList.add("iscript_viewerCardUi"); 238 | this.cardDisplay.append(cardUi); 239 | 240 | // Card ui double-click triggers rotation of the card 241 | cardUi.addEventListener("dblclick", async () => 242 | { 243 | cardUi.classList.remove("rotated" + pile.cards[i].rotation); 244 | pile.cards[i].rotation++; 245 | if (pile.cards[i].aspect !== 1) 246 | { 247 | pile.cards[i].rotation++; 248 | } 249 | if (pile.cards[i].rotation > 3) 250 | { 251 | pile.cards[i].rotation = 0; 252 | } 253 | cardUi.classList.add("rotated" + pile.cards[i].rotation); 254 | 255 | // Save the state 256 | expand("state save"); 257 | }); 258 | } 259 | 260 | // Setup a drag system to handle drag-reordering the cards 261 | this.dragReorder = 262 | new _inlineScripts.inlineScripts.HelperFncs.DragReorder( 263 | this.cardDisplay, this._onDragReordered); 264 | } 265 | 266 | // Event callback for reorder event of the drag system 267 | async onDragReordered() 268 | { 269 | // Get the current pile 270 | const pile = 271 | _inlineScripts.state.sessionState.cards.piles[this.pileSelect.value]; 272 | 273 | // Create a new cards array, based on the old one, but with the new card order 274 | let newCards = []; 275 | for (let i = this.cardDisplay.childNodes.length - 1; i >= 0; i--) 276 | { 277 | newCards.push(pile.cards[this.cardDisplay.childNodes[i].dataset.id]); 278 | this.cardDisplay.childNodes[i].dataset.id = newCards.length-1; 279 | } 280 | 281 | // Put new cards array in the current pile 282 | pile.cards = newCards; 283 | 284 | // Save the state 285 | expand("state save"); 286 | } 287 | 288 | // Return the string id for this panel type 289 | getViewType() { return CARDPILE_VIEW_TYPE; } 290 | // Return the caption for this panel type 291 | getDisplayText() { return "Inline Scripts - Card-pile view"; } 292 | // Return the string id for the iconfor this panel type 293 | getIcon() { return CARDPILE_VIEW_TYPE; } 294 | } 295 | 296 | // Register the panel type with the above class 297 | _inlineScripts.inlineScripts.HelperFncs.registerView( 298 | CARDPILE_VIEW_TYPE, leaf => new CardPileView(leaf)); 299 | ``` 300 | __ 301 | Sets up this shortcut-file 302 | 303 | 304 | __ 305 | ``` 306 | ^sfile shutdown$ 307 | ``` 308 | __ 309 | ```js 310 | // Event callback removal 311 | delete _inlineScripts.cards?.listeners?.onPileListChanged?.cards_pileViewer; 312 | delete _inlineScripts.cards?.listeners?.onPileChanged?.cards_pileViewer; 313 | 314 | // Custom CSS removal 315 | _inlineScripts.inlineScripts.HelperFncs.removeCss("cards_pileviewer"); 316 | 317 | // UI removal 318 | app.workspace.detachLeavesOfType(CARDPILE_VIEW_TYPE); 319 | 320 | ``` 321 | __ 322 | Shuts down this shortcut-file 323 | 324 | 325 | __ 326 | ``` 327 | ^cards? open viewer$ 328 | ``` 329 | __ 330 | ```js 331 | await app.workspace.getRightLeaf(false).setViewState({ type: CARDPILE_VIEW_TYPE }); 332 | return null; 333 | ``` 334 | __ 335 | cards open viewer - Open a panel for viewing card-piles. 336 | -------------------------------------------------------------------------------- /lists_ui.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | An extension to __lists.sfile__ that provides graphical ui versions of shortcuts. 6 | 7 | 8 | __ 9 | ``` 10 | ^ui lists? reset$ 11 | ``` 12 | __ 13 | ```js 14 | return expand("list reset"); 15 | ``` 16 | __ 17 | ui lists reset - Clears all lists. 18 | *** 19 | 20 | 21 | __ 22 | ``` 23 | ^ui lists?$ 24 | ``` 25 | __ 26 | ```js 27 | // Get the list of list names, early out empty 28 | let lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 29 | if (!lists.length) 30 | { 31 | return expand("lists"); 32 | } 33 | 34 | // Add "all lists" option 35 | lists.unshift(""); 36 | 37 | // Choose a list (or "all lists") 38 | const pick = popups.pick("Choose a list to view", lists, 0, "adaptive"); 39 | if (pick === null) { return null; } 40 | 41 | // return expansion of the list info (or ALL lists if pick is 0) 42 | return expand("lists" + (pick ? (" " + lists[pick]) : "")); 43 | ``` 44 | __ 45 | ui lists - User chooses a list (options include "all lists"), then shows info on the chosen list. 46 | 47 | 48 | __ 49 | ``` 50 | ^ui lists? rename$ 51 | ``` 52 | __ 53 | ```js 54 | // Get the list of list names, early out if empty 55 | let lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 56 | if (!lists.length) 57 | { 58 | return expFormat("List not renamed. No lists to rename."); 59 | } 60 | 61 | // Choose a list 62 | const pick = popups.pick("Choose a list to rename", lists, 0, "adaptive"); 63 | if (pick === null) { return null; } 64 | 65 | // Enter a new name, early out if not in a valid format 66 | const newName = 67 | popups.input("Enter a new name for __" + lists[pick] + "__", lists[pick]); 68 | if (newName === null) { return null; } 69 | if (!newName.match(/^[_a-zA-Z][_a-zA-Z0-9]*$/)) 70 | { 71 | return expFormat( 72 | "List item not added. List name __\"" + newName + "\"_ isn't valid."); 73 | } 74 | 75 | // If newName IS oldName, do nothing 76 | if (newName === lists[pick]) { return null; } 77 | 78 | return expand("lists rename " + lists[pick] + " " + newName); 79 | ``` 80 | __ 81 | ui lists rename - User chooses a list, and enters a new name for it. The list is renamed. 82 | 83 | 84 | __ 85 | ``` 86 | ^ui lists? removelist$ 87 | ``` 88 | __ 89 | ```js 90 | // Get the list of list names, early out if empty 91 | let lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 92 | if (!lists.length) 93 | { 94 | return expFormat("List not removed. No lists to remove."); 95 | } 96 | 97 | // Choose a list 98 | const pick = popups.pick("Choose a list to remove", lists, 0, "adaptive"); 99 | if (pick === null) { return null; } 100 | 101 | return expand("lists removelist " + lists[pick]); 102 | ``` 103 | __ 104 | ui lists removelist - User chooses a list, which is removed. 105 | 106 | 107 | __ 108 | __ 109 | ```js 110 | // get the items of a given list, regardless of list type. 111 | async function getListItems(name) 112 | { 113 | return expand("lists listraw " + name); 114 | } 115 | 116 | // returns the names of lists that have items. 117 | async function getNamesOfPopulatedLists() 118 | { 119 | let result = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 120 | return await _inlineScripts.inlineScripts.HelperFncs.asyncFilter( 121 | result, async v => (await getListItems(v)).length); 122 | }; 123 | 124 | // Filter list names to those of basic and combo type 125 | function filterListsToBasicAndCombo(lists) 126 | { 127 | return lists.filter(v => 128 | { 129 | const type = _inlineScripts.state.sessionState.lists[v].type; 130 | return (type === "basic" || type === "combo"); 131 | }); 132 | } 133 | ``` 134 | __ 135 | Helper scripts 136 | 137 | 138 | __ 139 | ``` 140 | ^ui lists? pick$ 141 | ``` 142 | __ 143 | ```js 144 | // Get the list of populated list's names, early out if empty 145 | const lists = await getNamesOfPopulatedLists(); 146 | if (!lists.length) 147 | { 148 | return expFormat("No item picked. There are no non-empty lists."); 149 | } 150 | 151 | // Choose a list 152 | const pick = 153 | popups.pick("Choose a list to randomly pick from", lists, 0, "adaptive"); 154 | if (pick === null) { return null; } 155 | 156 | return expand("lists pick " + lists[pick]); 157 | ``` 158 | __ 159 | ui lists pick - User chooses a non-empty list, a random item is picked from that list and returned. 160 | *** 161 | 162 | 163 | __ 164 | ``` 165 | ^ui lists? add$ 166 | ``` 167 | __ 168 | ```js 169 | // Get the list of list names 170 | let lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 171 | 172 | // Only include lists of basic and combo type 173 | lists = filterListsToBasicAndCombo(lists); 174 | 175 | // Choice defaults to 0 (which means creating a new list to add to) 176 | let listName = ""; 177 | let listItems = []; 178 | 179 | // If choices aren't empty, ask user to choose which list to add to 180 | if (lists.length) 181 | { 182 | // Add option for a new list 183 | lists.unshift(""); 184 | 185 | // Choose a list 186 | const pick = popups.pick("Choose a list to add to", lists, 0, "adaptive"); 187 | if (pick === null) { return null; } 188 | 189 | // If user's choice wasn't "new list", record the choice 190 | if (pick > 0) 191 | { 192 | listName = lists[pick]; 193 | listItems = [...new Set(await getListItems(listName))]; 194 | } 195 | } 196 | 197 | // If choice was "new list", OR there are no lists, get a name for the new list. 198 | if (listName === "") 199 | { 200 | // Enter name, early out if not in a valid format 201 | listName = popups.input("Enter a list to add to"); 202 | if (!listName) { return null; } 203 | if (!listName.match(/^[_a-zA-Z][_a-zA-Z0-9]*$/)) 204 | { 205 | return expFormat( 206 | "List item not added. List name __\"" + listName + "\"_ isn't valid."); 207 | } 208 | } 209 | 210 | // Get the item to add to the chosen list 211 | const item = 212 | popups.input("Type up an item to add to list " + listName + "", "", 213 | listItems); 214 | if (!item) { return null; } 215 | 216 | return expand("lists add " + listName + " " + item); 217 | ``` 218 | __ 219 | ui lists add - User chooses a list to add to and an item to add. The item is added to the list. 220 | - Identical items within a list are allowed. 221 | - Can only add to (1) basic lists and (2) combo lists with at least one basic list as a sub-list. 222 | 223 | 224 | __ 225 | ``` 226 | ^ui lists? replace$ 227 | ``` 228 | __ 229 | ```js 230 | // Get the list of populated list's names 231 | let lists = await getNamesOfPopulatedLists(); 232 | 233 | // Only include lists of basic and combo type, early out if empty 234 | lists = filterListsToBasicAndCombo(lists); 235 | if (!lists.length) 236 | { 237 | return expFormat("No item replaced. There are no non-empty lists."); 238 | } 239 | 240 | // Choose a list 241 | const pick = popups.pick("Choose a list to replace items from", lists, 0, "adaptive"); 242 | if (pick === null) { return null; } 243 | 244 | // Get items from the list 245 | let items = await getListItems(lists[pick]); 246 | 247 | // Count diplicate items 248 | let itemCounts = {}; 249 | for (const item of items) 250 | { 251 | itemCounts[item] = (itemCounts[item] || 0) + 1; 252 | } 253 | 254 | // Remove duplicate items 255 | items = [...new Set(items)]; 256 | let itemLabels = [...items]; 257 | 258 | // Add count to items with duplicates 259 | for (let i = 0; i < items.length; i++) 260 | { 261 | const count = itemCounts[items[i]]; 262 | itemLabels[i] = items[i] + (count <= 1 ? "" : " (x " + count + ")"); 263 | } 264 | 265 | // Choose an item 266 | const pick2 = popups.pick("Choose an item to replace.", itemLabels, 0, "adaptive"); 267 | if (pick2 === null) { return null; } 268 | const item = items[pick2]; 269 | 270 | // Eenter a new item text 271 | let replacement = 272 | popups.input("Enter text to replace \"" + item + "\" with.", item); 273 | if (!replacement) { return null; } 274 | 275 | return expand("lists replace " + lists[pick] + " \"" + item + "\" " + replacement); 276 | ``` 277 | __ 278 | ui lists replace - User chooses a list, an item from the list and enters text to replace the item with. All instances of the item are replaced with the text. 279 | - Can only replace from (1) basic lists and (2) combo lists that contain basic lists. 280 | 281 | 282 | __ 283 | ``` 284 | ^ui lists? remove$ 285 | ``` 286 | __ 287 | ```js 288 | // Get the list of populated list's names 289 | let lists = await getNamesOfPopulatedLists(); 290 | 291 | // Only include lists of basic and combo type, early out if empty 292 | lists = filterListsToBasicAndCombo(lists); 293 | if (!lists.length) 294 | { 295 | return expFormat("No item removed. There are no non-empty lists."); 296 | } 297 | 298 | // Choose a list 299 | const pick = popups.pick("Choose list to remove an item from", lists, 0, "adaptive"); 300 | if (pick === null) { return null; } 301 | 302 | // Get items from the list 303 | let items = await getListItems(lists[pick]); 304 | 305 | // Remove duplicate items 306 | items = [...new Set(items)]; 307 | 308 | // Choose an item from the list 309 | const pick2 = popups.pick("Choose an item to remove", items, 0, "adaptive"); 310 | if (pick2 === null) { return null; } 311 | 312 | return expand("lists remove " + lists[pick] + " " + items[pick2]); 313 | ``` 314 | __ 315 | ui lists remove - User choses a list and an item from the list. The last instance of the item is removed from the list. 316 | - Can only remove from (1) basic lists and (2) combo lists that contain basic lists. 317 | *** 318 | 319 | 320 | __ 321 | ``` 322 | ^ui lists? addfolder$ 323 | ``` 324 | __ 325 | ```js 326 | // Enter a list name, early out if not in a valid format or unavailable 327 | listName = popups.input("Enter a name for the new folder-list"); 328 | if (!listName) { return null; } 329 | if (!listName.match(/^[_a-zA-Z][_a-zA-Z0-9]*$/)) 330 | { 331 | return expFormat( 332 | "Folder-List not added. List name __\"" + listName + "\"_ isn't valid."); 333 | } 334 | if (_inlineScripts.state.sessionState.lists[listName]) 335 | { 336 | return expFormat( 337 | "Folder-list not added. Name __\"" + listName + "\"__ unavailable."); 338 | } 339 | 340 | // Get a list of folders 341 | const folders = 342 | Object.keys(app.vault.fileMap).filter(v => app.vault.fileMap[v].children); 343 | 344 | // Choose a folder 345 | const pick = 346 | popups.pick("Choose the folder to attach list " + listName + " to.", 347 | folders, 0, "adaptive"); 348 | if (pick === null) { return null; } 349 | 350 | return expand("lists addfolder " + listName + " " + folders[pick]); 351 | ``` 352 | __ 353 | ui lists addfolder - Asks the user to type a name for the new folder-list. 354 | Asks the user to choose a folder. 355 | Creates the folder-list, attached to the folder. 356 | 357 | 358 | __ 359 | ``` 360 | ^ui lists? addcombo$ 361 | ``` 362 | __ 363 | ```js 364 | // Enter a list name, early out if not in a valid format or unavailable 365 | listName = popups.input("Enter a name for the new combo-list"); 366 | if (!listName) { return null; } 367 | if (!listName.match(/^[_a-zA-Z][_a-zA-Z0-9]*$/)) 368 | { 369 | return expFormat( 370 | "Combo-List not added. List name __\"" + listName + "\"_ isn't valid."); 371 | } 372 | if (_inlineScripts.state.sessionState.lists[listName]) 373 | { 374 | return expFormat( 375 | "Combo-list not added. Name __\"" + listName + "\"__ unavailable."); 376 | } 377 | 378 | // Get the list of list names 379 | let lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 380 | 381 | // Get all lists to use as sub-lists 382 | let subLists = []; 383 | let noMoreMsg = ""; 384 | do 385 | { 386 | // Show selected lists so far 387 | const listsSoFar = 388 | !subLists.length ? "" : 389 | "

Lists so far:
- " + subLists.join("
- ") + "

"; 390 | 391 | // Pick the next sub-list 392 | let pick = 393 | popups.pick("Choose a list to link as a sub-list" + noMoreMsg + listsSoFar, 394 | lists, 0, "adaptive"); 395 | if (pick === null) { return null; } 396 | 397 | // After the first choice, add the option to finish the list of sub-lists 398 | if (!subLists.length) 399 | { 400 | lists.unshift(""); 401 | pick++; 402 | noMoreMsg = "
or choose \"<No more lists>\" if done linking"; 403 | } 404 | 405 | // A choice of 0 means "No more lists" was chosen 406 | if (pick === 0) 407 | { 408 | break; 409 | } 410 | 411 | // Add the chosen list to the sub-lists 412 | subLists.push(lists[pick]); 413 | } 414 | while (true); 415 | 416 | return expand("lists addcombo " + listName + " " + subLists.join(" ")); 417 | ``` 418 | __ 419 | ui lists addcombo - User enters a name for he comb-list and chooses sub-lists. A combo-list is made from the choices. 420 | *** 421 | 422 | 423 | __ 424 | ``` 425 | ^ui lists? shortcutbatch$ 426 | ``` 427 | __ 428 | ```js 429 | // Get the list of populated list's names, early out if empty 430 | const lists = await getNamesOfPopulatedLists(); 431 | if (!lists.length) 432 | { 433 | return expFormat("No shorcut batched. There are no non-empty lists."); 434 | } 435 | 436 | // Choose a list 437 | const pick = popups.pick("Choose a list for a batch shortut", lists, 0, "adaptive"); 438 | if (pick === null) { return null; } 439 | 440 | // Enter the shortcut text 441 | const batchShortcut = 442 | popups.input("Enter a shortcut-text. Put \"%1\" where the list item should be."); 443 | 444 | return expand("lists shortcutbatch " + lists[pick] + " " + batchShortcut); 445 | ``` 446 | __ 447 | ui lists shortcutbatch - User chooses a list and enters a shortcut-text. The shortcut-text is run for each item in the list, with "%1" being replaced by the list item. 448 | 449 | 450 | __ 451 | ``` 452 | ^ui lists? fromfile lines$ 453 | ``` 454 | __ 455 | ```js 456 | // Get the list of list names 457 | let lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 458 | 459 | // Only include lists of basic and combo type 460 | lists = filterListsToBasicAndCombo(lists); 461 | 462 | // Choice defaults to 0 (which means creating a new list to add to) 463 | let listName = ""; 464 | 465 | // If choices aren't empty, ask user to choose which list to add to 466 | if (lists.length) 467 | { 468 | // Add option for a new list 469 | lists.unshift(""); 470 | 471 | // Choose a list 472 | const pick = popups.pick("Choose a list to add to", lists, 0, "adaptive"); 473 | if (pick === null) { return null; } 474 | 475 | // If user's choice wasn't "new list", record the choice 476 | if (pick > 0) { listName = lists[pick]; } 477 | } 478 | 479 | // If choice was "new list", OR there are no lists, get a name for the new list. 480 | if (listName === "") 481 | { 482 | // Enter a new list name. Early out if not in a valid format 483 | listName = popups.input("Enter a list to add to"); 484 | if (!listName) { return null; } 485 | if (!listName.match(/^[_a-zA-Z][_a-zA-Z0-9]*$/)) 486 | { 487 | return expFormat( 488 | "List item not added. List name __\"" + listName + "\"_ isn't valid."); 489 | } 490 | } 491 | 492 | // Get a list of files 493 | const filenames = 494 | Object.keys(app.vault.fileMap).filter(v => !app.vault.fileMap[v].children); 495 | 496 | // Choose a file 497 | const pick2 = 498 | popups.pick("Choose the file to load lines from.", filenames, 0, "adaptive"); 499 | if (pick2 === null) { return null; } 500 | 501 | return expand("lists fromfile lines " + listName + " \"" + filenames[pick2] + "\""); 502 | ``` 503 | __ 504 | ui lists fromfile lines - Use chooses a list (or enters a new list) and chooses a file. The file is broken into lines, which are added to the list as items. 505 | -------------------------------------------------------------------------------- /mythicgme.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts for Mythic Game Master Emulator. Mythic GME is an excellent "GM emulator" system for solo and GM'less gaming. It was designed by Tana Pigeon. You can find more info about Mythic GME at [wordmill games](http://wordmillgames.com/mythic-game-master-emulator.html). 6 | 7 | This shortcut-file has a tutorial video available: 8 | - [Using the "mythicgme" shortcut-file to play Mythic GME](https://www.youtube.com/watch?v=NwHwctDp_vM) (runtime 5:00) 9 | 10 | Incompatible with __mythicv2.sfile__. If __mythicv2.sfile__ comes before __mythicgme.sfile__ in the shortcut-list, then __mythicgme.sfile__ will be disabled. 11 | 12 | Uses __state.sfile__ shortcut-file (optional). 13 | It uses this to save & load the chaos value and the scene count. 14 | 15 | Uses __lists.sfile__ shortcut-file (optional). 16 | Adds and uses lists for pcs, npcs and threads. 17 | This requires that __lists.sfile__ comes before __mythicgme.sfile__ in the shortcut-list. 18 | If the pc, npc and thread lists have items, then the __event__ and __detail__ shortcuts will incorporate them into their results. 19 | 20 | This shortcut-file has supplementary shortcut files: 21 | __mythicgme_ui.sfile__ - Graphical UI versions of some of these shortcuts. 22 | 23 | 24 | __ 25 | ``` 26 | ^sfile setup$ 27 | ``` 28 | __ 29 | ```js 30 | // Block loading if mythicv2 is already setup (incompatible shortcut-files) 31 | if (_inlineScripts.inlineScripts.sfileIndices["mythicv2"]) 32 | { 33 | print( 34 | "The mythicgme shortcut-file is disabled as it is incompatible with " + 35 | "the mythicv2 shortcut-file."); 36 | return true; 37 | } 38 | 39 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 40 | 41 | // Setup the state 42 | confirmObjectPath("_inlineScripts.state.sessionState.mythicgme.chaos", 5); 43 | confirmObjectPath("_inlineScripts.state.sessionState.mythicgme.scene", 1); 44 | 45 | // Setup the lists: pcs, npcs and threads 46 | confirmObjectPath( 47 | "_inlineScripts.state.sessionState.lists.pcs", { type: "basic", content: [] }); 48 | confirmObjectPath( 49 | "_inlineScripts.state.sessionState.lists.npcs", { type: "basic", content: [] }); 50 | confirmObjectPath( 51 | "_inlineScripts.state.sessionState.lists.threads",{ type: "basic", content: [] }); 52 | 53 | // Event callback - state.onReset 54 | confirmObjectPath( 55 | "_inlineScripts.state.listeners.onReset.mythicgme", 56 | function() 57 | { 58 | expand("mythicgme reset noconfirm"); 59 | }); 60 | ``` 61 | __ 62 | Sets up this shortcut-file 63 | 64 | 65 | __ 66 | ``` 67 | ^sfile shutdown$ 68 | ``` 69 | __ 70 | ```js 71 | // Event callback removal 72 | delete _inlineScripts.state?.listeners?.onReset?.mythicgme; 73 | 74 | // State removal 75 | delete _inlineScripts.state?.sessionState?.mythicgme; 76 | ``` 77 | __ 78 | Shuts down this shortcut-file 79 | 80 | 81 | __ 82 | ``` 83 | ^mythic(?:gme)? reset$ 84 | ``` 85 | __ 86 | ```js 87 | // Confirm 88 | if (!popups.confirm("Confirm resetting the Mythic GME system")) 89 | { 90 | return null; 91 | } 92 | 93 | // Reset 94 | return expand("mythicgme reset noconfirm"); 95 | ``` 96 | __ 97 | mythicgme reset - Resets mythic state to defaults and displays scene heading. 98 | *** 99 | 100 | 101 | __ 102 | ``` 103 | ^mythic(?:gme)? reset noconfirm$ 104 | ``` 105 | __ 106 | ```js 107 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 108 | 109 | // Reset state 110 | confirmObjectPath("_inlineScripts.state.sessionState.mythicgme"); 111 | _inlineScripts.state.sessionState.mythicgme.chaos = 5; 112 | _inlineScripts.state.sessionState.mythicgme.scene = 1; 113 | 114 | // Recreate lists: pcs, npcs, threads 115 | confirmObjectPath("_inlineScripts.state.sessionState.lists"); 116 | _inlineScripts.state.sessionState.lists.pcs = 117 | { type: "basic", content: [] }; 118 | _inlineScripts.state.sessionState.lists.npcs = 119 | { type: "basic", content: [] }; 120 | _inlineScripts.state.sessionState.lists.threads = 121 | { type: "basic", content: [] }; 122 | 123 | // Return a heading for scene 1 124 | return "***\n\n\n### SCENE " + 125 | _inlineScripts.state.sessionState.mythicgme.scene + "\n- Setup:\n - "; 126 | ``` 127 | __ 128 | hidden - No-confirm reset 129 | 130 | 131 | __ 132 | __ 133 | ```js 134 | // Make a roll from 1 to max. 135 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 136 | 137 | // Pick an item from array a. 138 | function aPick(a) { return a[roll(a.length)-1]; } 139 | 140 | // Pick an item from array a, weighted by element wIndex of the item. If theRoll is 141 | // passed, use that as the roll. 142 | function aPickWeight(a, wIndex, theRoll) 143 | { 144 | wIndex = wIndex || 1; 145 | theRoll = theRoll || roll(a.last()[wIndex]); 146 | for (const item of a) 147 | { 148 | if (item[wIndex] >= theRoll) 149 | { 150 | return item; 151 | } 152 | } 153 | return a.last(); 154 | } 155 | ``` 156 | __ 157 | Helper scripts 158 | 159 | 160 | __ 161 | ``` 162 | ^fate ?(|-4|-3|-2|-1|0|1|2|3|4|5|6)$ 163 | ``` 164 | __ 165 | ```js 166 | // Parameter defaults to 0 167 | $1 = Number($1) || 0; 168 | 169 | // Data 170 | const ODDS = ["impossible","no way","very unlikely","unlikely","50/50","somewhat likely","likely","very likely","near sure thing","sure thing","has to be"]; 171 | const FATE_CHART = [ 172 | [ [ 10, 50, 91],[ 5, 25, 86],[ 3, 15, 84],[ 2, 10, 83],[ 1, 5, 82],[ 1, 5, 82],[ 0, 0, 81],[ 0, 0, 81],[ 0,-20, 77] ], 173 | [ [ 15, 75, 96],[ 10, 50, 91],[ 7, 35, 88],[ 5, 25, 86],[ 3, 15, 84],[ 2, 10, 83],[ 1, 5, 82],[ 1, 5, 82],[ 0, 0, 81] ], 174 | [ [ 16, 85, 97],[ 13, 65, 94],[ 10, 50, 91],[ 9, 45, 90],[ 5, 25, 86],[ 3, 15, 84],[ 2, 10, 83],[ 1, 5, 82],[ 1, 5, 82] ], 175 | [ [ 18, 90, 99],[ 15, 75, 96],[ 11, 55, 92],[ 10, 50, 91],[ 7, 35, 88],[ 4, 20, 85],[ 3, 15, 84],[ 2, 10, 83],[ 1, 5, 82] ], 176 | [ [ 19, 95,100],[ 16, 85, 97],[ 15, 75, 96],[ 13, 65, 94],[ 10, 50, 91],[ 7, 35, 88],[ 5, 25, 86],[ 3, 15, 84],[ 2, 10, 83] ], 177 | [ [ 19, 95,100],[ 18, 90, 99],[ 16, 85, 97],[ 16, 80, 97],[ 13, 65, 94],[ 10, 50, 91],[ 9, 45, 90],[ 5, 25, 86],[ 4, 20, 85] ], 178 | [ [ 20,100, 0],[ 19, 95,100],[ 18, 90, 99],[ 16, 85, 97],[ 15, 75, 96],[ 11, 55, 92],[ 10, 50, 91],[ 7, 35, 88],[ 5, 25, 86] ], 179 | [ [ 21,105, 0],[ 19, 95,100],[ 19, 95,100],[ 18, 90, 99],[ 16, 85, 97],[ 15, 75, 96],[ 13, 65, 94],[ 10, 50, 91],[ 9, 45, 90] ], 180 | [ [ 23,115, 0],[ 20,100, 0],[ 19, 95,100],[ 19, 95,100],[ 18, 90, 99],[ 16, 80, 97],[ 15, 75, 96],[ 11, 55, 92],[ 10, 50, 91] ], 181 | [ [ 25,125, 0],[ 22,110, 0],[ 19, 95,100],[ 19, 95,100],[ 18, 90, 99],[ 16, 85, 97],[ 16, 80, 97],[ 13, 65, 94],[ 11, 55, 92] ], 182 | [ [ 26,145, 0],[ 26,130, 0],[ 20,100, 0],[ 20,100, 0],[ 19, 95,100],[ 19, 95,100],[ 18, 90, 99],[ 16, 85, 97],[ 16, 80, 97] ] ]; 183 | 184 | // Determine the fate range from the "odds" parameter and the current chaos level 185 | const fateRange = 186 | FATE_CHART[$1+4][9 - _inlineScripts.state.sessionState.mythicgme.chaos]; 187 | 188 | // Roll on the fate range to determine fate result 189 | const r = roll(100); 190 | let result = 191 | (r <= fateRange[0]) ? "EXTEREME YES" : 192 | (r <= fateRange[1]) ? "YES" : 193 | (r <= fateRange[2]) ? "NO" : 194 | "EXTREME NO"; 195 | 196 | // If the roll triggers a random event, add it 197 | let eventOutput = ""; 198 | if (Math.trunc(r/10) == r % 10 && 199 | r % 10 < _inlineScripts.state.sessionState.mythicgme.chaos) 200 | { 201 | eventOutput = "\nevent - " + expand("event")[1]; 202 | } 203 | 204 | // Return the fate result, along with the odds and possible random event 205 | return expFormat("Fate check (" + ODDS[$1+4] + "):\n" + result + eventOutput); 206 | ``` 207 | __ 208 | fate {odds: -4 TO 6 ("impossible" TO "has to be"), default: 0 ("50/50")} - Makes a fate check based on {odds}. 209 | 210 | 211 | __ 212 | ``` 213 | ^fate ?\s?(.*)$ 214 | ``` 215 | __ 216 | ```js 217 | // Data 218 | const INPUT_ODDS = 219 | { 220 | "impossible": -4, "no way": -3, "very unlikely": -2, "unlikely": -1, "50/50": 0, "somewhat likely": 1, "likely": 2, "very likely": 3, "near sure thing": 4, "sure thing": 5, "has to be": 6 221 | }; 222 | 223 | // Turn the odds parameter from text to number, then call the other fate shortcut 224 | return expand("fate " + (INPUT_ODDS[$1.trim().toLowerCase()] ?? 0)); 225 | ``` 226 | __ 227 | fate {odds: text, default: "50/50"} - Makes a fate check based on {odds}: a specific text, such as "impossible", "sure thing", etc. 228 | 229 | __ 230 | ``` 231 | ^scene get?$ 232 | ``` 233 | __ 234 | ```js 235 | return expFormat( 236 | "Currently in scene " + _inlineScripts.state.sessionState.mythicgme.scene + "."); 237 | ``` 238 | __ 239 | scene get - Shows the current scene. 240 | 241 | 242 | __ 243 | ``` 244 | ^scene (1|-1)$ 245 | ``` 246 | __ 247 | ```js 248 | // Adjust the chaos by the parameter 249 | let result = 250 | ($1 === "1") ? expUnformat(expand("chaos++")) : 251 | ($1 === "-1") ? expUnformat(expand("chaos--")) : 252 | "Chaos is unchanged at " + expUnformat(expand("chaos")); 253 | 254 | // Prefix the scene heading with a bunch of space 255 | result += "\n\n\n***\n\n\n"; 256 | 257 | // Advance the scene index 258 | _inlineScripts.state.sessionState.mythicgme.scene++; 259 | 260 | // Add the scene header 261 | result += "### SCENE " + _inlineScripts.state.sessionState.mythicgme.scene; 262 | 263 | // Roll scene check for scene control 264 | let chk = roll(10); 265 | if (chk <= _inlineScripts.state.sessionState.mythicgme.chaos) 266 | { 267 | // Odd roll? Modify the scene in some way 268 | if (chk % 2) 269 | { 270 | result += "\n- Scene altered"; 271 | } 272 | // Even Roll? Replace the scene with a random event 273 | else 274 | { 275 | result += "\n- Scene interrupted:\n - event - " + expand("event")[1]; 276 | } 277 | } 278 | 279 | // Return the scene header 280 | return result + "\n- setup:\n - "; 281 | ``` 282 | __ 283 | scene {chaosAdjust: -1 OR 1} - Shifts the chaos value by {chaosAdjust}, then increments the current scene and runs a scene check. 284 | *** 285 | 286 | 287 | __ 288 | ``` 289 | ^event$ 290 | ``` 291 | __ 292 | ```js 293 | // Data 294 | const FOCUS_TABLE = [ ["REMOTE",7],["NPC ACTS",28,"npcs"],["NEW NPC",35],["THREAD ADVANCE",45,"threads"],["THREAD LOSS",52,"threads"],["THREAD END",55,"threads"],["PC NEGATIVE",67,"pcs"],["PC POSITIVE",75,"pcs"],["AMBIGUOUS",83],["NPC NEGATIVE",92,"npcs"],["NPC POSITIVE",100,"npcs"] ]; 295 | 296 | // Roll on the table 297 | let result = aPickWeight(FOCUS_TABLE); 298 | 299 | // If the table result has an associated list, add a random pick from the list 300 | let focus = result[2] ? expand("lists pick " + result[2]) : ""; 301 | focus = (focus.length < 2) ? "" : (" (" + focus[1] + ")"); 302 | 303 | // Add a roll from the meaning tables 304 | let meaning = expand("meaning")[1]; 305 | 306 | // Return the random event result 307 | return expFormat([ "Event:\n", result[0] + focus + " - " + meaning, "" ]); 308 | ``` 309 | __ 310 | event - Makes an event check. 311 | 312 | 313 | __ 314 | ``` 315 | ^meaning$ 316 | ``` 317 | __ 318 | ```js 319 | // Data 320 | const ACTION_TABLE = ["ATTAINMENT","STARTING","NEGLECT","FIGHT","RECRUIT","TRIUMPH","VIOLATE","OPPOSE","MALICE","COMMUNICATE","PERSECUTE","INCREASE","DECREASE","ABANDON","GRATIFY","INQUIRE","ANTAGONISE","MOVE","WASTE","TRUCE","RELEASE","BEFRIEND","JUDGE","DESERT","DOMINATE","PROCRASTINATE","PRAISE","SEPARATE","TAKE","BREAK","HEAL","DELAY","STOP","LIE","RETURN","IMMITATE","STRUGGLE","INFORM","BESTOW","POSTPONE","EXPOSE","HAGGLE","IMPRISON","RELEASE","CELEBRATE","DEVELOP","TRAVEL","BLOCK","HARM","DEBASE","OVERINDULGE","ADJOURN","ADVERSITY","KILL","DISRUPT","USURP","CREATE","BETRAY","AGREE","ABUSE","OPPRESS","INSPECT","AMBUSH","SPY","ATTACH","CARRY","OPEN","CARELESSNESS","RUIN","EXTRAVAGANCE","TRICK","ARRIVE","PROPOSE","DIVIDE","REFUSE","MISTRUST","DECEIVE","CRUELTY","INTOLERANCE","TRUST","EXCITEMENT","ACTIVITY","ASSIST","CARE","NEGLIGENCE","PASSION","WORK_HARD","CONTROL","ATTRACT","FAILURE","PURSUE","VENGEANCE","PROCEEDINGS","DISPUTE","PUNISH","GUIDE","TRANSFORM","OVERTHROW","OPPRESS","CHANGE"]; 321 | const SUBJECT_TABLE = ["GOALS","DREAMS","ENVIRONMENT","OUTSIDE","INSIDE","REALITY","ALLIES","ENEMIES","EVIL","GOOD","EMOTIONS","OPPOSITION","WAR","PEACE","THE_INNOCENT","LOVE","THE_SPIRITUAL","THE_INTELLECTUAL","NEW_IDEAS","JOY","MESSAGES","ENERGY","BALANCE","TENSION","FRIENDSHIP","THE_PHYSICAL","A_PROJECT","PLEASURES","PAIN","POSSESSIONS","BENEFITS","PLANS","LIES","EXPECTATIONS","LEGAL_MATTERS","BUREAUCRACY","BUSINESS","A_PATH","NEWS","EXTERIOR_FACTORS","ADVICE","A_PLOT","COMPETITION","PRISON","ILLNESS","FOOD","ATTENTION","SUCCESS","FAILURE","TRAVEL","JEALOUSY","DISPUTE","HOME","INVESTMENT","SUFFERING","WISHES","TACTICS","STALEMATE","RANDOMNESS","MISFORTUNE","DEATH","DISRUPTION","POWER","A_BURDEN","INTRIGUES","FEARS","AMBUSH","RUMOR","WOUNDS","EXTRAVAGANCE","A_REPRESENTATIVE","ADVERSITIES","OPULENCE","LIBERTY","MILITARY","THE_MUNDANE","TRIALS","MASSES","VEHICLE","ART","VICTORY","DISPUTE","RICHES","STATUS_QUO","TECHNOLOGY","HOPE","MAGIC","ILLUSIONS","PORTALS","DANGER","WEAPONS","ANIMALS","WEATHER","ELEMENTS","NATURE","THE_PUBLIC","LEADERSHIP","FAME","ANGER","INFORMATION"]; 322 | 323 | // Roll on the two meaning tables 324 | let result = aPick(ACTION_TABLE) + " _(of)_ " + aPick(SUBJECT_TABLE); 325 | 326 | // Return the meaning roll result 327 | return expFormat([ "Meaning:\n", result, "" ]); 328 | ``` 329 | __ 330 | meaning - Rolls on the meaning tables. 331 | *** 332 | 333 | 334 | __ 335 | ``` 336 | ^chaos$ 337 | ``` 338 | __ 339 | ```js 340 | // Return the current chaos value, wrapped in a nice message 341 | return expFormat( 342 | [ "Chaos is __", _inlineScripts.state.sessionState.mythicgme.chaos, "__." ]); 343 | ``` 344 | __ 345 | chaos - Shows the current chaos value. 346 | 347 | 348 | __ 349 | ``` 350 | ^chaos--$ 351 | ``` 352 | __ 353 | ```js 354 | // Lower the chaos value 355 | _inlineScripts.state.sessionState.mythicgme.chaos--; 356 | 357 | // Floor the chaos value (with expansion notifying user of the flooring) 358 | if (_inlineScripts.state.sessionState.mythicgme.chaos < 1) 359 | { 360 | _inlineScripts.state.sessionState.mythicgme.chaos = 1; 361 | return expFormat("Chaos remains at __1__ (hit minimum)."); 362 | } 363 | 364 | return expFormat( 365 | "Chaos lowered to __" + _inlineScripts.state.sessionState.mythicgme.chaos + 366 | "__."); 367 | ``` 368 | __ 369 | chaos-- - Decreases the chaos value by 1 (minimum of 1). 370 | 371 | 372 | __ 373 | ``` 374 | ^chaos\+\+$ 375 | ``` 376 | __ 377 | ```js 378 | // Raise the chaos value 379 | _inlineScripts.state.sessionState.mythicgme.chaos++; 380 | 381 | // Ceiling the chaos value (with expansion notifying user of the ceiling) 382 | if (_inlineScripts.state.sessionState.mythicgme.chaos > 9) 383 | { 384 | _inlineScripts.state.sessionState.mythicgme.chaos = 9; 385 | return expFormat("Chaos remains at __9__ (hit maximum)."); 386 | } 387 | 388 | return expFormat( 389 | "Chaos raised to __" + _inlineScripts.state.sessionState.mythicgme.chaos + "__."); 390 | ``` 391 | __ 392 | chaos++ - Increases the chaos value by 1 (maximum of 9). 393 | 394 | 395 | __ 396 | ``` 397 | ^chaos=([1-9])$ 398 | ``` 399 | __ 400 | ```js 401 | // Explicity set the chaos to the parameter 402 | _inlineScripts.state.sessionState.mythicgme.chaos = $1; 403 | 404 | return expFormat("Chaos set to __" + $1 + "__."); 405 | ``` 406 | __ 407 | chaos={value: 1 TO 9} - Sets the chaos value to {value}. 408 | 409 | 410 | __ 411 | ``` 412 | ^lists? pick (|[_a-zA-Z][_a-zA-Z0-9]*)(| [1-9][0-9]*)$ 413 | ``` 414 | __ 415 | ```js 416 | return []; 417 | ``` 418 | __ 419 | hidden - A placeholder shortcut in case the shortcut-file lists.sfile isn't available. 420 | -------------------------------------------------------------------------------- /lists.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts for working with lists. 6 | 7 | This shortcut-file has a tutorial video available: 8 | - [Using the "list" shortcut-file to manage lists](https://www.youtube.com/watch?v=xIYpnBKdYRg) (runtime 5:34) 9 | 10 | Uses __state.sfile__ shortcut-file (optional). 11 | It uses this to save & load the lists. 12 | 13 | This shortcut-file has supplementary shortcut files: 14 | __lists_ui.sfile__ - Graphical UI versions of some of these shortcuts. 15 | 16 | 17 | __ 18 | ``` 19 | ^sfile setup$ 20 | ``` 21 | __ 22 | ```js 23 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 24 | 25 | // Check that the state is initialized 26 | confirmObjectPath("_inlineScripts.state.sessionState.lists"); 27 | 28 | // Event callback for state.onReset 29 | confirmObjectPath( 30 | "_inlineScripts.state.listeners.onReset.lists", 31 | function() 32 | { 33 | expand("lists reset noconfirm"); 34 | }); 35 | ``` 36 | __ 37 | Sets up this shortcut-file 38 | 39 | 40 | __ 41 | ``` 42 | ^sfile shutdown$ 43 | ``` 44 | __ 45 | ```js 46 | //Event callback removal 47 | delete _inlineScripts?.state?.listeners?.onReset?.lists; 48 | 49 | // State removal 50 | delete _inlineScripts?.state?.sessionState?.lists; 51 | ``` 52 | __ 53 | Shuts down this shortcut-file 54 | 55 | 56 | __ 57 | ``` 58 | ^lists? reset$ 59 | ``` 60 | __ 61 | ```js 62 | // Confirm 63 | if (!popups.confirm("Confirm resetting the Lists system")) { return null; } 64 | 65 | // Reset 66 | expand("lists reset noconfirm"); 67 | 68 | return expFormat("All lists cleared."); 69 | ``` 70 | __ 71 | lists reset - Clears all lists. 72 | *** 73 | 74 | 75 | __ 76 | ``` 77 | ^lists? reset noconfirm$ 78 | ``` 79 | __ 80 | ```js 81 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 82 | 83 | // Confirm that the state is initialized 84 | confirmObjectPath("_inlineScripts.state.sessionState"); 85 | 86 | // Reset lists 87 | _inlineScripts.state.sessionState.lists = {}; 88 | ``` 89 | __ 90 | hidden - No-confirm reset 91 | 92 | 93 | __ 94 | __ 95 | ```js 96 | function caseInsensitiveSort(lhs, rhs) 97 | { 98 | return lhs.toLowerCase().localeCompare(rhs.toLowerCase()); 99 | } 100 | 101 | // a function to get the items of a given list, for all possible list types. 102 | function getListItems(name) 103 | { 104 | // The list to get items for 105 | let list = _inlineScripts.state.sessionState.lists[name]; 106 | if (!list) { return []; } 107 | 108 | // Handle different list types 109 | switch (list.type) 110 | { 111 | // basic type - contain their items in their "content" property 112 | case "basic": 113 | return list.content.sort(caseInsensitiveSort); 114 | break; 115 | // folder type - items are the files in the associated folder ("content" prop) 116 | case "folder": 117 | // Get folder and confirm it's valid 118 | const targetFile = app.fileManager.vault.fileMap[list.content]; 119 | if (!targetFile || !targetFile.children) 120 | { 121 | return []; 122 | } 123 | // Return links to folder children that are notes 124 | return [... targetFile.children ] 125 | .filter(v => !v.children) 126 | .filter(v => v.name.endsWith(".md")) 127 | .map(v => "[[" + v.basename + "]]") 128 | .sort(caseInsensitiveSort); 129 | // combo type - items are from the sub-lists of this combo ("content" prop) 130 | case "combo": 131 | { 132 | let result = []; 133 | // Iterate over lists and add items from each 134 | for (const sublist of list.content) 135 | { 136 | result = result.concat(getListItems(sublist)); 137 | } 138 | // Sort and return the resulting list 139 | return result.sort(caseInsensitiveSort); 140 | } 141 | } 142 | } 143 | ``` 144 | __ 145 | Helper scripts 146 | 147 | 148 | __ 149 | ``` 150 | ^lists?$ 151 | ``` 152 | __ 153 | ```js 154 | // Get all lists names 155 | const lists = Object.keys(_inlineScripts.state.sessionState.lists).sort(); 156 | 157 | // Start the expansion 158 | let result = "Lists:"; 159 | 160 | // Early out with the result if no lists 161 | if (!lists.length) 162 | { 163 | return expFormat(result + "\n- NONE"); 164 | } 165 | 166 | // Add each list's individual expansion to the final expansion 167 | for (let i = 0; i < lists.length; i++) 168 | { 169 | // Individual list's expansion (but without the header, and as a single string) 170 | const listExpansion = expand("lists " + lists[i]).slice(1).join(""); 171 | // Unformat the individual list expansion, replace period-bullets with indented 172 | // dash-bullets, then add it to the final expansion. 173 | result += 174 | "\n- " + expUnformat(listExpansion).replaceAll("\n. ", "\n - "); 175 | } 176 | 177 | return expFormat(result); 178 | ``` 179 | __ 180 | lists - Shows all lists and their info: type, associations and items. 181 | 182 | 183 | __ 184 | ``` 185 | ^lists? ([_a-zA-Z][_a-zA-Z0-9]*)$ 186 | ``` 187 | __ 188 | ```js 189 | // Create items list, or "NONE" if no items 190 | let itemText = getListItems($1); 191 | itemText = (itemText?.length ? (". " + itemText.join("\n. ")) : ". NONE"); 192 | 193 | // Create the type info. If list isn't registered, type defaults to basic. 194 | let typeText = _inlineScripts.state.sessionState.lists[$1]?.type || "basic"; 195 | // No type info for basic types 196 | if (typeText === "basic") 197 | { 198 | typeText = ""; 199 | } 200 | // Non-basic types show the list's type and associations. For folder-list, 201 | // association is the folder. For combo-list, association is the sub-lists. 202 | else 203 | { 204 | const associations = _inlineScripts.state.sessionState.lists[$1].content; 205 | typeText = " _(" + typeText + ": " + associations + ")_"; 206 | } 207 | 208 | return expFormat([ "List ", "__" + $1 + "__" + typeText + ":\n", itemText, "" ]); 209 | ``` 210 | __ 211 | lists {list name: name text} - Shows all info for the single list {list name}: type, associations and items. 212 | 213 | 214 | __ 215 | ``` 216 | ^lists? rename ([_a-zA-Z][_a-zA-Z0-9]*) ([_a-zA-Z][_a-zA-Z0-9]*)$ 217 | ``` 218 | __ 219 | ```js 220 | // Confirm that old list name is valid and new list name is NOT valid 221 | if (!_inlineScripts.state.sessionState.lists[$1]) 222 | { 223 | return expFormat("List not renamed. List __" + $1 + "__ not found."); 224 | } 225 | if (_inlineScripts.state.sessionState.lists[$2]) 226 | { 227 | return expFormat("List not renamed. List name __" + $2 + "__ is already used."); 228 | } 229 | 230 | // Copy the list to the new list name 231 | _inlineScripts.state.sessionState.lists[$2] = _inlineScripts.state.sessionState.lists[$1]; 232 | 233 | // Remove the old list name 234 | delete _inlineScripts.state.sessionState.lists[$1]; 235 | 236 | return expFormat("List __" + $1 + "__ renamed to __" + $2 + "__."); 237 | ``` 238 | __ 239 | lists rename {original list name: name text} {new list name: name text} - Changes the name of list {original list name} to {new list name}. 240 | 241 | 242 | __ 243 | ``` 244 | ^lists? removelist ([_a-zA-Z][_a-zA-Z0-9]*)$ 245 | ``` 246 | __ 247 | ```js 248 | // Error out if specified list doesn't exist 249 | if (!_inlineScripts.state.sessionState.lists[$1]) 250 | { 251 | return expFormat("List __" + $1 + "__ not removed. List does not exist."); 252 | } 253 | 254 | // Remove the list 255 | delete _inlineScripts.state.sessionState.lists[$1]; 256 | 257 | return expFormat("List __" + $1 + "__ removed."); 258 | ``` 259 | __ 260 | lists removelist {list name: name text} - Removes the entire list {list name}. 261 | 262 | 263 | __ 264 | ``` 265 | ^lists? pick ([_a-zA-Z][_a-zA-Z0-9]*)$ 266 | ``` 267 | __ 268 | ```js 269 | // Helper function - Make a roll from 1 to max. 270 | function roll(max) { return Math.trunc(Math.random() * max + 1); } 271 | 272 | // Get the number of items. Early out if no items 273 | let itemCount = getListItems($1).length; 274 | if (itemCount === 0) 275 | { 276 | return expFormat([ "Failed to pick from list __" + $1 + "__. List is empty." ]); 277 | } 278 | 279 | // Use the unlisted pick shortcut, which takes the index to pick. The unlisted 280 | // version is used to handle the randomness externally. It is used by the 281 | // adventurecrafter shortcut-file. 282 | return expand("lists pick " + $1 + " " + roll(itemCount)); 283 | ``` 284 | __ 285 | lists pick {list name: name text} - Gets a random item from the list {list name}. 286 | 287 | 288 | __ 289 | ``` 290 | ^lists? pick ([_a-zA-Z][_a-zA-Z0-9]*) ([1-9][0-9]*)$ 291 | ``` 292 | __ 293 | ```js 294 | // Convert $2 from 1-index to 0-index 295 | $2 = Number($2) - 1; 296 | 297 | // Generic expansion messages 298 | const ERROR_MSG_PREFIX = "Item not picked from list __" + $1 + "__. "; 299 | 300 | // Get the list's items. Early out if parameter is out-of-bounds 301 | let items = getListItems($1); 302 | if ($2 >= items.length) 303 | { 304 | return expFormat( 305 | [ ERROR_MSG_PREFIX + "The item index " + $2 + " is out of range." ]); 306 | } 307 | 308 | // Return expansion with the picked item 309 | return expFormat([ "__", items[$2], "__ picked from list __", $1, "__." ]); 310 | ``` 311 | __ 312 | hidden - lists pick {list name: name text} {item index: >0} - Gets the item number {item index} from the list {list name}. 313 | *** 314 | 315 | 316 | __ 317 | ``` 318 | ^lists? add ([_a-zA-Z][_a-zA-Z0-9]*) (.+)$ 319 | ``` 320 | __ 321 | ```js 322 | // If the given list isn't yet defined, auto-define it as a basic list. 323 | _inlineScripts.state.sessionState.lists[$1] ||= { type: "basic", content: [] }; 324 | 325 | // Generic expansion messages 326 | const SUCCESS_MSG = "Item __" + $2 + "__ added to list __" + $1 + "__."; 327 | const ERROR_MSG_PREFIX = "Item __" + $2 + "__ not added to list __" + $1 + "__. "; 328 | 329 | // Add is handled differently for different list types 330 | switch (_inlineScripts.state.sessionState.lists[$1].type) 331 | { 332 | // Basic type - Just append the item 333 | case "basic": 334 | _inlineScripts.state.sessionState.lists[$1].content.push($2); 335 | return expFormat([ SUCCESS_MSG ]); 336 | // Combo type - Append the item to the last possible sub-list 337 | case "combo": 338 | { 339 | // Iterate sub-lists IN REVERSE ORDER, to add to the LAST possible sublist 340 | const sublists = _inlineScripts.state.sessionState.lists[$1].content; 341 | for (let i = sublists.length-1; i >= 0; i--) 342 | { 343 | // Try adding 344 | const result = expand("lists add " + sublists[i] + " " + $2); 345 | // if add was successful (signified by size 1), expand success message 346 | if (result.length === 1) 347 | { 348 | return expFormat([ SUCCESS_MSG ]); 349 | } 350 | } 351 | // If failed to add to any sub-list, expand error message 352 | return expFormat([ ERROR_MSG_PREFIX, "No sub-lists support adding." ]); 353 | } 354 | // Any other type doesn't support adding 355 | default: 356 | return expFormat([ ERROR_MSG_PREFIX, "List-type doesn't support adding." ]); 357 | } 358 | ``` 359 | __ 360 | lists add {list name: name text} {item: text} - Adds {item} to the list {list name}. Allows duplicate items. 361 | - Can only add to (1) basic lists and (2) combo lists which contain basic lists. 362 | 363 | 364 | __ 365 | ``` 366 | ^lists? replace ([_a-zA-Z][_a-zA-Z0-9]*) ("[^ ].*"|[^ ]+) (.+)$ 367 | ``` 368 | __ 369 | ```js 370 | // Remove any quotes around the old item 371 | $2 = $2.replace(/^"(.*)"$/, "$1") 372 | 373 | // Generic expansion messages 374 | const SUCCESS_MSG = 375 | "All __" + $2 + "__ replaced with __" + $3 + "__ in list __" + $1 + "__."; 376 | const ERROR_MSG_PREFIX = "Item __" + $2 + "__ not replaced in list __" + $1 + "__. "; 377 | 378 | // Confirm that the list exists 379 | if (!_inlineScripts.state.sessionState.lists[$1]) 380 | { 381 | return expFormat(ERROR_MSG_PREFIX + "List __" + $1 + "__ not found."); 382 | } 383 | 384 | // Relacement is handled differently for different list types 385 | switch (_inlineScripts.state.sessionState.lists[$1].type) 386 | { 387 | // Basic type - iterate over items, replacing any matches 388 | case "basic": 389 | { 390 | const items = _inlineScripts.state.sessionState.lists[$1].content; 391 | for (let i = 0; i < items.length; i++) 392 | { 393 | if (items[i] === $2) 394 | { 395 | items[i] = $3; 396 | } 397 | } 398 | return expFormat(SUCCESS_MSG); 399 | } 400 | // combo type - iterate over sub-lists, running the replacement for each 401 | case "combo": 402 | { 403 | const sublists = _inlineScripts.state.sessionState.lists[$1].content; 404 | for (const sublist of sublists) 405 | { 406 | expand("lists replace " + sublist + " " + $2 + " " + $3); 407 | } 408 | return expFormat(SUCCESS_MSG); 409 | } 410 | // Any other type doesn't support replacement 411 | default: 412 | return expFormat(ERROR_MSG_PREFIX + "List-type doesn't support replacement."); 413 | } 414 | ``` 415 | __ 416 | lists replace {list name: name text} {item: text} {replacement: text} - Replaces all instances of {item} with {replacement}. {item} must be quoted if it has any space. 417 | 418 | 419 | __ 420 | ``` 421 | ^lists? remove ([_a-zA-Z][_a-zA-Z0-9]*) (.+)$ 422 | ``` 423 | __ 424 | ```js 425 | // Generic expansion messages 426 | const SUCCESS_MSG = "__" + $2 + "__ removed from list __" + $1 + "__."; 427 | const ERROR_MSG_PREFIX = 428 | "Item __" + $2 + "__ not removed from list __" + $1 + "__. "; 429 | 430 | // Confirm that the list exists 431 | if (!_inlineScripts.state.sessionState.lists[$1]) 432 | { 433 | return expFormat(ERROR_MSG_PREFIX + "List not found."); 434 | } 435 | 436 | // Remove is handled differently for different list types 437 | switch (_inlineScripts.state.sessionState.lists[$1].type) 438 | { 439 | // Basic type - Iterate over items backwards, removing the last instance found 440 | case "basic": 441 | { 442 | const items = _inlineScripts.state.sessionState.lists[$1].content; 443 | for (let i = items.length-1; i >= 0; i--) 444 | { 445 | if (items[i] === $2) 446 | { 447 | items.splice(i, 1); 448 | return expFormat([ SUCCESS_MSG ]); 449 | } 450 | } 451 | // If we get here, no items were removed 452 | return expFormat([ ERROR_MSG_PREFIX, "Item not found." ]); 453 | } 454 | // Combo type - Iterate over sub-items backwards, running remove until successful. 455 | case "combo": 456 | { 457 | const sublists = _inlineScripts.state.sessionState.lists[$1].content; 458 | for (let i = sublists.length-1; i >= 0; i--) 459 | { 460 | const result = expand("lists remove " + sublists[i] + " " + $2); 461 | if (result.length === 1) 462 | { 463 | return expFormat([ SUCCESS_MSG ]); 464 | } 465 | } 466 | // If we get here, no items were removed 467 | return expFormat([ ERROR_MSG_PREFIX, "Unable to remove from any sub-list." ]); 468 | } 469 | // Any other type doesn't support removal 470 | default: 471 | return expFormat([ ERROR_MSG_PREFIX, "List type doesn't support removal." ]); 472 | } 473 | ``` 474 | __ 475 | lists remove {list name: name text} {item: text} - Removes an instance of {item} from the list {list name}. 476 | - Can only remove from (1) basic lists and (2) combo lists which contain basic lists. 477 | *** 478 | 479 | 480 | __ 481 | ``` 482 | ^lists? addfolder ([_a-zA-Z][_a-zA-Z0-9]*) ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+)$ 483 | ``` 484 | __ 485 | ```js 486 | // Remove any quotes around the specified folder path 487 | $2 = $2.replace(/^"(.*)"$/, "$1") 488 | 489 | // Confirm the list name is available 490 | if (_inlineScripts.state.sessionState.lists[$1]) 491 | { 492 | return expFormat("File-list not created. Name __\"" + $1 + "\"__ unavailable."); 493 | } 494 | 495 | // Create the folder-list 496 | _inlineScripts.state.sessionState.lists[$1] = { type: "folder", content: $2 }; 497 | 498 | return expFormat("Folder-list __" +$1 + "__ added, linked to __\"" + $2 + "\"__."); 499 | ``` 500 | __ 501 | lists addfolder {list name: name text} {folder: path text} - Creates a folder-list named {list name} that is linked to the folder {folder}. A "folder-list" is a list who's items are the names of the notes in the linked folder. 502 | 503 | 504 | __ 505 | ``` 506 | ^lists? addcombo ([_a-zA-Z][_a-zA-Z0-9]*) ([_a-zA-Z][ _a-zA-Z0-9]*)$ 507 | ``` 508 | __ 509 | ```js 510 | // Turn the sub-lists parameter into an array of (non-empty) names 511 | let sublists = $2.split(" ").filter(v => v); 512 | 513 | // Confirm the list name is available 514 | if (_inlineScripts.state.sessionState.lists[$1]) 515 | { 516 | return expFormat("Combo-list not created. Name __\"" + $1 + "\"__ unavailable."); 517 | } 518 | 519 | // Create the combo-list 520 | _inlineScripts.state.sessionState.lists[$1] = { type: "combo", content: sublists }; 521 | 522 | return expFormat( 523 | "Combo-list __" + $1 + "__ added, linked to:\n. " + sublists.join("\n. ")); 524 | ``` 525 | __ 526 | lists addcombo {list name: name text} {sub-list: name text separated by spaces} - Creates a combo-list named {list name} that is linked to the lists given in {sub-list}. A "combo-list" is a list who's items are all of the items of its linked sub-lists. 527 | *** 528 | 529 | 530 | __ 531 | ``` 532 | ^lists? shortcutbatch ([_a-zA-Z][_a-zA-Z0-9]*) (.*)$ 533 | ``` 534 | __ 535 | ```js 536 | // Check that the specified list is valid 537 | if (!_inlineScripts.state.sessionState.lists[$1]) 538 | { 539 | return expFormat("Shortcut batch not run. List __" + $1 + "__ not found."); 540 | } 541 | 542 | // Begin the expansion 543 | let result = "Shortcut batch for list __" + $1 + "__...\n\n"; 544 | 545 | // Run the shortcut once for each item in the list, adding the result to the expansion 546 | for (const item of getListItems($1)) 547 | { 548 | result += expUnformat( expand($2.replaceAll("%1", item)) ) + "\n\n"; 549 | } 550 | 551 | // End and return the expansion 552 | return expFormat(result + "... shortcut batch finished."); 553 | ``` 554 | __ 555 | lists shortcutbatch {list name: name text} {shortcut: text} - Runs shortcut {shortcut} once for each item in list {list name}, replacing "%1" in {shortcut} with the item. 556 | 557 | 558 | __ 559 | ``` 560 | ^lists? fromfile lines ([_a-zA-Z][_a-zA-Z0-9]*) ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+)$ 561 | ``` 562 | __ 563 | ```js 564 | // Remove any quotes around the file path 565 | $2 = $2.replace(/^"(.*)"$/, "$1") 566 | 567 | // Get the specified file 568 | let file = app.vault.fileMap[$2]; 569 | 570 | // If file isn't valid, try adding an "md" extension 571 | if (!file || file.children) 572 | { 573 | file = app.vault.fileMap[$2 + ".md"]; 574 | } 575 | 576 | // If file still isn't valid, give up 577 | if (!file) 578 | { 579 | return expFormat("Lines not added. File __" + $2 + "__ not found."); 580 | } 581 | if (file.children) 582 | { 583 | return expFormat("Lines not added. __" + $2 + "__ is a folder."); 584 | } 585 | 586 | // Get the file's lines 587 | const content = await app.vault.cachedRead(file); 588 | const lines = content.split("\n").filter(v => v); 589 | 590 | // Add the file's lines 591 | for (const line of lines) 592 | { 593 | const result = expand("lists add " + $1 + " " + line); 594 | if (result.length > 1) 595 | { 596 | return expFormat( 597 | "Lines not added. Type of list __" + $1 + "__ can't be added to."); 598 | } 599 | } 600 | 601 | return expFormat("__" + lines.length + "__ items added to list __" + $1 + "__."); 602 | ``` 603 | __ 604 | lists fromfile lines {list name: name text} {file: path text} - Takes the file {file}, breaks it up into individual lines and adds each of those lines, as items, to list {list name}. 605 | 606 | 607 | __ 608 | ``` 609 | ^lists? listraw ([_a-zA-Z][_a-zA-Z0-9]*)$ 610 | ``` 611 | __ 612 | ```js 613 | // Return the array of items directly 614 | return getListItems($1); 615 | ``` 616 | __ 617 | hidden - Returns an array of the items in a list without formatting. Useful internally (as a sub-shortcut). 618 | 619 | 620 | __ 621 | ``` 622 | ^lists? type ([_a-zA-Z][_a-zA-Z0-9]*)$ 623 | ``` 624 | __ 625 | ```js 626 | // Get the specified list 627 | const list = inlineScripts.state.sessionState.lists[$1]; 628 | 629 | // If list doesn't exist, return type of "none" 630 | if (!list) { return "none"; } 631 | 632 | // Return the list's type 633 | return list.type; 634 | ``` 635 | __ 636 | hidden - gets the type of the list named {list name}. Useful internally (as a sub-shortcut). 637 | -------------------------------------------------------------------------------- /cards_ui.sfile.md: -------------------------------------------------------------------------------- 1 | An extension to __cards.sfile__ that provides graphical ui versions of shortcuts. 2 | 3 | This shortcut-file has a tutorial video available: 4 | [Using the "cards" shortcut-file to use virtual cards](https://www.youtube.com/watch?v=KkpjTL2UvtQ) (runtime 12:43) 5 | 6 | 7 | __ 8 | ``` 9 | ^sfile setup$ 10 | ``` 11 | __ 12 | ```js 13 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 14 | 15 | // Helper function - Turn relative path into absolute path based in the vault's root 16 | function getAbsolutePath(path) 17 | { 18 | if (path.startsWith("data:image")) { return path; } 19 | path = app.vault.fileMap[path]; 20 | if (!path) { return ""; } 21 | return app.vault.getResourcePath(path); 22 | } 23 | 24 | // Helper function - Get the current back-image, url 25 | function getBackImage() 26 | { 27 | return _inlineScripts.state.sessionState.cards.backImage || 28 | _inlineScripts.cards.defaultBackImage; 29 | } 30 | 31 | // Custom popop definition for various card manipulations 32 | confirmObjectPath("_inlineScripts.cards_ui.manipulateCardsPopup", 33 | { 34 | // Setup function 35 | onOpen: async (data, parent, firstButton, SettingType) => 36 | { 37 | data.pileNames = Object.keys(_inlineScripts.state.sessionState.cards.piles); 38 | 39 | // Source pile dropdown 40 | new SettingType(parent) 41 | .setName( 42 | data.defaults?.dst === undefined ? "Card-pile" : "Source card-pile") 43 | .setDesc(data.descriptions?.src) 44 | .addDropdown(dropdown => 45 | { 46 | dropdown.addOptions(data.pileNames); 47 | dropdown.setValue( 48 | data.defaults?.src ? 49 | data.pileNames.indexOf(data.defaults?.src) : 50 | 0); 51 | data.src = dropdown; 52 | dropdown.selectEl.addEventListener("keypress", e => 53 | { 54 | if (e.key === "Enter") { firstButton.click(); } 55 | }); 56 | return dropdown; 57 | }); 58 | 59 | // Destination pile dropdown 60 | if (data.defaults?.dst !== undefined) 61 | { 62 | new SettingType(parent) 63 | .setName("Destination Card-pile") 64 | .setDesc(data.descriptions?.dst) 65 | .addDropdown(dropdown => 66 | { 67 | dropdown.addOptions(data.pileNames); 68 | dropdown.setValue( 69 | data.defaults?.dst ? 70 | data.pileNames.indexOf(data.defaults?.dst) : 71 | 0); 72 | data.dst = dropdown; 73 | dropdown.selectEl.addEventListener("keypress", e => 74 | { 75 | if (e.key === "Enter") { firstButton.click(); } 76 | }); 77 | return dropdown; 78 | }); 79 | } 80 | 81 | // Count textbox 82 | if (data.defaults?.count !== undefined) 83 | { 84 | new SettingType(parent) 85 | .setName("Count") 86 | .setDesc(data.descriptions?.count) 87 | .addText(text => 88 | { 89 | text.setValue(data.defaults?.count + ""); 90 | data.count = text; 91 | text.inputEl.addEventListener("keypress", e => 92 | { 93 | if (e.key === "Enter") { firstButton.click(); } 94 | else if (e.keyCode < 48 || e.keyCode > 57) 95 | { 96 | window.event.returnValue = null; 97 | } 98 | }); 99 | text.inputEl.addEventListener("blur", e => 100 | { 101 | if (Number(e.target.value) < 1) 102 | { 103 | e.target.value = "1"; 104 | } 105 | }); 106 | return text; 107 | }); 108 | } 109 | 110 | // Topbottom dropdown 111 | if (data.defaults?.topBottom !== undefined) 112 | { 113 | new SettingType(parent) 114 | .setName("Top or bottom") 115 | .setDesc(data.descriptions?.topBottom) 116 | .addDropdown(dropdown => 117 | { 118 | dropdown.addOptions([ "Top", "Bottom" ]); 119 | dropdown.setValue(data.defaults?.topBottom); 120 | data.topBottom = dropdown; 121 | dropdown.selectEl.addEventListener("keypress", e => 122 | { 123 | if (e.key === "Enter") { firstButton.click(); } 124 | }); 125 | return dropdown; 126 | }); 127 | } 128 | 129 | // Rotate toggle button 130 | if (data.defaults?.rotate !== undefined) 131 | { 132 | new SettingType(parent) 133 | .setName("Rotate cards") 134 | .setDesc(data.descriptions?.rotate) 135 | .addToggle(toggle => 136 | { 137 | toggle.setValue(data.defaults?.rotate); 138 | data.rotate = toggle; 139 | toggle.toggleEl.addEventListener("keypress", e => 140 | { 141 | if (e.key === "Enter") { firstButton.click(); } 142 | }); 143 | return toggle; 144 | }); 145 | } 146 | }, 147 | 148 | // Shutown function 149 | onClose: async (data, resolveFnc, buttonId) => 150 | { 151 | if (buttonId !== "Ok") { return; } 152 | let result = { src: data.pileNames[data.src.getValue()] }; 153 | if (data.dst) { result.dst = data.pileNames[data.dst.getValue()]; } 154 | if (data.count) { result.count = data.count.getValue(); } 155 | if (data.topBottom) { result.topBottom = data.topBottom.getValue(); } 156 | if (data.rotate) { result.rotate = data.rotate.getValue(); } 157 | resolveFnc(result); 158 | } 159 | }); 160 | 161 | // Custom popop definition for altering the settings of the cards system 162 | confirmObjectPath("_inlineScripts.cards_ui.settingsPopup", 163 | { 164 | // Setup function 165 | onOpen: async (data, parent, firstButton, SettingType) => 166 | { 167 | // The current card size (before we change it) 168 | const currentSize = _inlineScripts.state.sessionState.cards.size; 169 | const currentBackImage = getBackImage(); 170 | 171 | // A parent div to center the card visualization 172 | let sampleContainerUi = document.createElement("div"); 173 | sampleContainerUi.style["text-align"] = "center"; 174 | parent.append(sampleContainerUi); 175 | 176 | // The card visualization 177 | data.sampleUi = document.createElement("img"); 178 | data.sampleUi.src = getAbsolutePath(currentBackImage); 179 | data.sampleUi.style.width = currentSize + "px"; 180 | sampleContainerUi.append(data.sampleUi); 181 | 182 | // A slider to change the card size 183 | let sizeUi = new SettingType(parent) 184 | .setName("Size (" + currentSize + ")") 185 | .setDesc("Pick the size for all cards (in pixels).") 186 | .addSlider((slider) => 187 | { 188 | slider 189 | .setLimits(10, 500, 10) 190 | .setValue(currentSize) 191 | .onChange(value => 192 | { 193 | data.sampleUi.style.width = value + "px"; 194 | data.titleUi.innerText = "Size (" + value + ")"; 195 | }); 196 | data.sliderUi = slider; 197 | slider.sliderEl.addEventListener("keypress", e => 198 | { 199 | if (e.key === "Enter") { firstButton.click(); } 200 | }); 201 | return slider; 202 | }); 203 | // Keep track of the slider label - to change its text when the size changes 204 | data.titleUi = sizeUi.nameEl; 205 | 206 | // A dropdown to select the card back 207 | let backImageUi = new SettingType(parent) 208 | .setName("Back image") 209 | .setDesc("Choose the image to represent the back of all cards.") 210 | .addDropdown(dropdown => 211 | { 212 | data.backImageOptions = 213 | Object.keys(app.vault.fileMap).filter( 214 | v => { return v.match(/.(?:png|bmp|gif|jpg|jpeg)$/); } 215 | ); 216 | data.backImageOptions.unshift(""); 217 | dropdown.addOptions(data.backImageOptions); 218 | dropdown.setValue(data.backImageOptions.indexOf(currentBackImage)); 219 | dropdown.onChange(value => 220 | { 221 | data.sampleUi.src = getAbsolutePath( 222 | data.backImageOptions[value] || 223 | _inlineScripts.cards.defaultBackImage); 224 | }); 225 | data.backImageUi = dropdown; 226 | dropdown.selectEl.addEventListener("keypress", e => 227 | { 228 | if (e.key === "Enter") { firstButton.click(); } 229 | }); 230 | return dropdown; 231 | }); 232 | }, 233 | 234 | // Shutown function 235 | onClose: async (data, resolveFnc, buttonId) => 236 | { 237 | if (buttonId !== "Ok") { return; } 238 | resolveFnc({ 239 | size: data.sliderUi.getValue(), 240 | backImage: data.backImageOptions[data.backImageUi.getValue()] || "default" 241 | }); 242 | } 243 | }); 244 | ``` 245 | __ 246 | Sets up this shortcut-file 247 | 248 | 249 | __ 250 | __ 251 | ```js 252 | // User chooses an existing pile 253 | userChoosesPileId = function(requestMessage, failMessage, pileToBlock) 254 | { 255 | // Get names of the piles 256 | const pileNames = 257 | Object.keys(_inlineScripts.state.sessionState.cards.piles). 258 | filter(v => v !== pileToBlock); 259 | if (!pileNames.length) 260 | { 261 | return [ null, expFormat(failMessage + "No card-piles available.") ]; 262 | } 263 | 264 | // Choose a pile 265 | const pileIndex = 266 | popups.pick("Choose a card-pile " + requestMessage, pileNames, 0, "adaptive"); 267 | if (pileIndex === null) { return [ null, null ]; } 268 | 269 | // Return the chosen pile name 270 | return [ pileNames[pileIndex], null ]; 271 | } 272 | 273 | // Return true if at least one pile exists, return false otherwise 274 | function doPilesExist() 275 | { 276 | return !!( Object.keys(_inlineScripts.state.sessionState.cards.piles).length ); 277 | } 278 | ``` 279 | __ 280 | Helper scripts 281 | 282 | 283 | __ 284 | ``` 285 | ^ui cards? reset$ 286 | ``` 287 | __ 288 | ```js 289 | return expand("cards reset"); 290 | ``` 291 | __ 292 | ui cards reset - Clears all card-piles. 293 | 294 | 295 | __ 296 | ``` 297 | ^ui cards? settings$ 298 | ``` 299 | __ 300 | ```js 301 | // Choose settings 302 | const newSettings = popups.custom( 303 | "Choose settings for all cards", _inlineScripts.cards_ui.settingsPopup); 304 | if (newSettings === null) { return null; } 305 | 306 | return expand("cards settings " + newSettings.size + " " + newSettings.backImage); 307 | ``` 308 | __ 309 | ui cards settings - User chooses card size and card back image. 310 | *** 311 | 312 | 313 | __ 314 | ``` 315 | ^ui cards? pile$ 316 | ``` 317 | __ 318 | ```js 319 | // Get the data-string to import 320 | const toImport = popups.input("Enter a name for the new pile"); 321 | if (toImport === null) { return null; } 322 | if (_inlineScripts.state.sessionState.cards.piles[toImport]) 323 | { 324 | return expFormat("Card-pile not created. __" + toImport + "__ already exists."); 325 | } 326 | 327 | // User decides whether to rotate cards 328 | const isFaceUp = popups.pick( 329 | "How should cards face in the card-pile?", [ "Face-down", "Face-up" ], 0, 2); 330 | if (isFaceUp === null) { return null; } 331 | 332 | // User decides whether to rotate cards 333 | const hideMoved = popups.pick( 334 | "Should cards be printed when drawn or picked into the card-pile", 335 | [ "Yes", "No" ], _inlineScripts.state.sessionState.cards.priorShowMoved ? 0 : 1, 336 | 2); 337 | if (hideMoved === null) { return null; } 338 | 339 | // Pile creation shortcut is called 340 | return expand( 341 | "cards pile " + toImport + " " + (isFaceUp ? "up" : "down") + " " + 342 | (hideMoved ? "n" : "y")); 343 | ``` 344 | __ 345 | ui cards pile - User enters a name, card-facing and show-moved flag for the new card-pile. Card-pile is created from the choices. 346 | 347 | 348 | __ 349 | ``` 350 | ^ui cards? remove$ 351 | ``` 352 | __ 353 | ```js 354 | // User chooses the pile to remove 355 | const pile = await userChoosesPileId("to remove", "Card-pile not removed."); 356 | if (pile[0] == null) { return pile[1]; } 357 | 358 | // User decides whether to rotate cards 359 | const doRecall = popups.pick( 360 | "Try recalling card-pile's cards before removing it?", [ "Yes", "No" ], 0, 2); 361 | if (doRecall === null) { return null; } 362 | 363 | // Shuffle shortcut is called 364 | return expand("cards remove " + pile[0] + " " + (doRecall ? "n" : "y")); 365 | ``` 366 | __ 367 | ui cards remove - User choses a card-pile to remove, then removes that card-pile. 368 | 369 | 370 | __ 371 | ``` 372 | ^ui cards? pilesettings$ 373 | ``` 374 | __ 375 | ```js 376 | // User chooses the pile to change settings for 377 | const pileName = 378 | await userChoosesPileId("to change settings for", "Card-pile not changed."); 379 | if (pileName[0] == null) { return pileName[1]; } 380 | 381 | // Get the pile to use it's current settings for defaults 382 | const pile = _inlineScripts.state.sessionState.cards.piles[pileName[0]]; 383 | 384 | // User decides whether to rotate cards 385 | const isFaceUp = popups.pick( 386 | "How should cards face in the card-pile?", [ "Face-down", "Face-up" ], 387 | pile.isFaceUp ? 1 : 0, 2); 388 | if (isFaceUp === null) { return null; } 389 | 390 | // User decides whether to rotate cards 391 | const hideMoved = popups.pick( 392 | "Should cards be printed when drawn or picked into the card-pile", 393 | [ "Yes", "No" ], pile.showMoved ? 0 : 1, 2); 394 | if (hideMoved === null) { return null; } 395 | 396 | // Pile creation shortcut is called 397 | return expand( 398 | "cards pilesettings " + pileName[0] + " " + (isFaceUp ? "up" : "down") + " " + 399 | (hideMoved ? "n" : "y")); 400 | ``` 401 | __ 402 | ui cards pilesettings - User chooses a card-pile, then choses its card-facing and show-moved flag. 403 | *** 404 | 405 | 406 | __ 407 | ``` 408 | ^ui cards? fromfolder$ 409 | ``` 410 | __ 411 | ```js 412 | // Make a list of viable folders to choose from 413 | const folders = Object.keys(app.vault.fileMap) 414 | .filter(v => app.vault.fileMap[v].children?.length) 415 | // Choose folders with non-markdown files 416 | .filter(v => 417 | { 418 | for (const child of app.vault.fileMap[v].children) 419 | { 420 | if (child.extension && child.extension !== "md") 421 | { 422 | return true; 423 | } 424 | } 425 | return false; 426 | }); 427 | 428 | // Choose a folder to create cards from 429 | let folder = 430 | popups.pick("Choose a folder to take card images from", folders, 0, "adaptive"); 431 | if (!folder) { return null; } 432 | folder = folders[folder]; 433 | 434 | // Choose a pile to put the cards into 435 | const pileId = 436 | await userChoosesPileId("to hold new cards", "Card images not loaded."); 437 | if (pileId === null) { return pile[1]; } 438 | 439 | return expand("cards fromfolder " + pileId[0] + " " + folder); 440 | ``` 441 | __ 442 | ui cards fromfolder - User choses a folder to create new cards from, then choses a card-pile to put them into. Cards are created and added to the chosen card-pile. 443 | *** 444 | 445 | 446 | __ 447 | ``` 448 | ^ui cards? list$ 449 | ``` 450 | __ 451 | ```js 452 | return expand("cards list"); 453 | ``` 454 | __ 455 | ui cards list - Lists all card-piles. 456 | 457 | 458 | __ 459 | ``` 460 | ^ui cards? peek$ 461 | ``` 462 | __ 463 | ```js 464 | // Show a custom popup 465 | const data = 466 | { 467 | defaults: 468 | { 469 | src: _inlineScripts.state.sessionState.cards.priorPeekPile || "", 470 | count: 1, 471 | topBottom: 0 472 | }, 473 | descriptions: 474 | { 475 | src: "Card-pile to peek into", 476 | count: "How many cards to peek", 477 | topBottom: "Peek at top or bottom of card-pile" 478 | } 479 | }; 480 | const result = popups.custom( 481 | "Choose peek options", _inlineScripts.cards_ui.manipulateCardsPopup, data); 482 | if (result === null) { return null; } 483 | 484 | // Run the non-ui shortcut 485 | return expand( 486 | "cards peek " + result.count + " " + result.src + " " + 487 | (result.topBottom === "1" ? "y" : "n")); 488 | ``` 489 | __ 490 | ui cards peek - User choses a card-pile, how many cards to peek and what side to peek from (top or bottom). The peeked cards are printed to the note. 491 | *** 492 | 493 | 494 | __ 495 | ``` 496 | ^ui cards? draw$ 497 | ``` 498 | __ 499 | ```js 500 | // Show a custom popup 501 | const data = 502 | { 503 | defaults: 504 | { 505 | src: _inlineScripts.state.sessionState.cards.priorDrawSrc || "", 506 | dst: _inlineScripts.state.sessionState.cards.priorDrawDst || "", 507 | count: 1, 508 | }, 509 | descriptions: 510 | { 511 | src: "Card-pile to draw from", 512 | dst: "Card-pile to add to", 513 | count: "How many cards to draw" 514 | } 515 | }; 516 | const result = popups.custom( 517 | "Choose draw options", _inlineScripts.cards_ui.manipulateCardsPopup, data); 518 | if (result === null) { return null; } 519 | 520 | // Run the non-ui shortcut 521 | return expand("cards draw " + result.count + " " + result.dst + " " + result.src); 522 | ``` 523 | __ 524 | ui cards draw - User choses one card-pile to draw from, one to add to and how many cards to draw. Cards are moved from the top of one card-pile to the top of the other. 525 | 526 | 527 | __ 528 | ``` 529 | ^ui cards? pick$ 530 | ``` 531 | __ 532 | ```js 533 | // Show a custom popup 534 | const data = 535 | { 536 | defaults: 537 | { 538 | src: _inlineScripts.state.sessionState.cards.priorPickSrc || "", 539 | dst: _inlineScripts.state.sessionState.cards.priorPickDst || "" 540 | }, 541 | descriptions: 542 | { 543 | src: "Card-pile to pick from", 544 | dst: "Card-pile to add to" 545 | } 546 | }; 547 | const result = popups.custom( 548 | "Choose pick options", _inlineScripts.cards_ui.manipulateCardsPopup, data); 549 | if (result === null) { return null; } 550 | 551 | // Run the non-ui shortcut 552 | return expand("cards pick " + result.dst + " " + result.src); 553 | ``` 554 | __ 555 | ui cards pick - User choses one card-pile to pick from and one to add to, then the user picks the cards to move from the top of one to the top of the other. 556 | *** 557 | 558 | 559 | __ 560 | ``` 561 | ^ui cards? shuffle$ 562 | ``` 563 | __ 564 | ```js 565 | // Show a custom popup 566 | const data = 567 | { 568 | defaults: 569 | { 570 | src: 0, 571 | rotate: false 572 | }, 573 | descriptions: 574 | { 575 | src: "Card-pile to shuffle", 576 | rotate: "Rotate cards while shuffling?" 577 | } 578 | }; 579 | const result = popups.custom( 580 | "Choose pick options", _inlineScripts.cards_ui.manipulateCardsPopup, data); 581 | if (result === null) { return null; } 582 | 583 | // Run the non-ui shortcut 584 | return expand("cards shuffle " + result.src + " " + (result.rotate ? "y" : "n")); 585 | ``` 586 | __ 587 | ui cards shuffle - User choses a card-pile, and whether to rotate the cards while shuffling. The card-pile is then shuffled. 588 | 589 | 590 | __ 591 | ``` 592 | ^ui cards? unrotate$ 593 | ``` 594 | __ 595 | ```js 596 | const pile = await userChoosesPileId("to un-rotate", "Cards not un-rotated."); 597 | if (pile[0] == null) { return pile[1]; } 598 | return expand("cards unrotate " + pile[0]); 599 | ``` 600 | __ 601 | ui cards unrotate - User choses a card-pile, then all the card-pile's cards are turned right-side-up. 602 | 603 | 604 | __ 605 | ``` 606 | ^ui cards? reverse$ 607 | ``` 608 | __ 609 | ```js 610 | const pile = await userChoosesPileId("to reverse", "Card-pile not reversed."); 611 | if (pile[0] == null) { return pile[1]; } 612 | return expand("cards reverse " + pile[0]); 613 | ``` 614 | __ 615 | ui cards reverse - User choses a card-pile, then the card-pile's order is reversed. 616 | 617 | 618 | __ 619 | ``` 620 | ^ui cards? recall$ 621 | ``` 622 | __ 623 | ```js 624 | const pile = await userChoosesPileId("to recall", "Cards not recalled."); 625 | if (pile[0] == null) { return pile[1]; } 626 | return expand("cards recall " + pile[0]); 627 | ``` 628 | __ 629 | ui cards recall - User choses a card-pile, then the card-pile is recalled. Recalling means finding all cards with the card-pile as their origin, then moving them to the card-pile. 630 | 631 | 632 | __ 633 | ``` 634 | ^ui cards? reorigin$ 635 | ``` 636 | __ 637 | ```js 638 | const pile = await userChoosesPileId("to re-origin", "Cards not re-origined."); 639 | if (pile[0] == null) { return pile[1]; } 640 | return expand("cards reorigin " + pile[0]); 641 | ``` 642 | __ 643 | ui cards reorigin - Asks the user to choose a card-pile, then the card-pile is re-origined.. Re-origining means setting the origin of all cards in a card-pile to that card-pile. 644 | *** 645 | 646 | 647 | __ 648 | ``` 649 | ^ui cards? import$ 650 | ``` 651 | __ 652 | ```js 653 | // Get the pile to import into 654 | let pile = 655 | await userChoosesPileId("to import into", "Card-pile not imported."); 656 | if (pile[0] == null) { return pile[1]; } 657 | pile = pile[0]; 658 | 659 | // Get the pile's data-string for the default value 660 | let defaultDataString = 661 | JSON.stringify(_inlineScripts.state.sessionState.cards.piles[pile]); 662 | 663 | // Get the data-string to import 664 | const toImport = popups.input("Enter the data-string to import", defaultDataString); 665 | if (toImport === null) { return null; } 666 | if (toImport === defaultDataString) { return null; } 667 | 668 | // Run the import shortcut 669 | return expand("cards reorigin " + pile + " " + toImport); 670 | ``` 671 | __ 672 | ui cards import - User chooses a card-pile and enters a data-string representation of a card-pile. 673 | The data-string is turned into a card-pile and assigned to the card-pile the user chose. 674 | 675 | 676 | __ 677 | ``` 678 | ^ui cards? export$ 679 | ``` 680 | __ 681 | ```js 682 | const pile = await userChoosesPileId("to export", "Card-pile not exported."); 683 | if (pile[0] == null) { return pile[1]; } 684 | return expand("cards export " + pile[0]); 685 | ``` 686 | __ 687 | ui cards export - User chooses a card-pile, then the card-pile is exported to a data-string, which is printed to the note. 688 | -------------------------------------------------------------------------------- /mythicv2.sfile.md: -------------------------------------------------------------------------------- 1 | --- 2 | obsidianUIMode: preview 3 | --- 4 | 5 | Shortcuts for Mythic Variations 2. Mythic GME, along with it's "Variations 2" supplement, is an excellent "GM emulator" system for solo and GM'less gaming. It was designed by Tana Pigeon. You can find more info about Mythic Variations 2 at [wordmill games](http://wordmillgames.com/mythic-variations-2.html). 6 | 7 | This shortcut-file has a tutorial video available: 8 | - [Using the "mythicv2" shortcut-file to play Mythic Variations 2](https://www.youtube.com/watch?v=fvrxAg22lYg) (runtime 7:27) 9 | 10 | Incompatible with __mythicgme.sfile__. If __mythicgme.sfile__ comes before __mythicv2.sfile__ in the shortcut-list, then __mythicv2.sfile__ will be disabled. 11 | 12 | Uses __state.sfile__ shortcut-file (optional). 13 | It uses this to save & load the chaos value, the scene count, and "Details" mode. 14 | 15 | Uses __lists.sfile__ shortcut-file (optional). 16 | Adds and uses lists for pcs, npcs and threads. 17 | This requires that __lists.sfile__ comes before __mythicv2.sfile__ in the shortcut-list. 18 | If the pc, npc and thread lists have items, then the __event__ and __detail__ shortcuts will incorporate them into their results. 19 | 20 | This shortcut-file has supplementary shortcut files: 21 | __mythicv2_ui.sfile__ - Graphical UI versions of some of these shortcuts. 22 | 23 | 24 | __ 25 | ``` 26 | ^sfile setup$ 27 | ``` 28 | __ 29 | ```js 30 | // Block loading if mythicgme is already setup (incompatible shortcut-files) 31 | if (_inlineScripts.inlineScripts.sfileIndices["mythicgme"]) 32 | { 33 | print( 34 | "The mythicv2 shortcut-file is disabled as it is incompatible with " + 35 | "the mythicgme shortcut-file."); 36 | return true; 37 | } 38 | 39 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 40 | 41 | // Setup the state 42 | confirmObjectPath("_inlineScripts.state.sessionState.mythicv2.chaos", 4); 43 | confirmObjectPath("_inlineScripts.state.sessionState.mythicv2.scene", 1); 44 | 45 | // Setup the lists: pcs, npcs, threads 46 | confirmObjectPath( 47 | "_inlineScripts.state.sessionState.lists.pcs", { type: "basic", content: [] }); 48 | confirmObjectPath( 49 | "_inlineScripts.state.sessionState.lists.npcs", { type: "basic", content: [] }); 50 | confirmObjectPath( 51 | "_inlineScripts.state.sessionState.lists.threads",{ type: "basic", content: [] }); 52 | 53 | // Setup the session-specific state 54 | confirmObjectPath("_inlineScripts.mythicv2.details", []); 55 | 56 | // Event callback - state.onReset 57 | confirmObjectPath( 58 | "_inlineScripts.state.listeners.onReset.mythicv2", 59 | function() 60 | { 61 | expand("mythicv2 reset noconfirm"); 62 | }); 63 | ``` 64 | __ 65 | Sets up this shortcut-file 66 | 67 | 68 | __ 69 | ``` 70 | ^sfile shutdown$ 71 | ``` 72 | __ 73 | ```js 74 | // Event callback removal 75 | delete _inlineScripts.state?.listeners?.onReset?.mythicv2; 76 | 77 | // State removal 78 | delete _inlineScripts.state?.sessionState?.mythicv2; 79 | ``` 80 | __ 81 | Shuts down this shortcut-file 82 | 83 | 84 | __ 85 | ``` 86 | ^mythic(?:v2)? reset$ 87 | ``` 88 | __ 89 | ```js 90 | // Confirm 91 | if (!popups.confirm("Confirm resetting the Mythic Version 2 system")) 92 | { 93 | return null; 94 | } 95 | 96 | // Reset 97 | return expand("mythicv2 reset noconfirm"); 98 | ``` 99 | __ 100 | mythicv2 reset - Resets mythic state to defaults and displays scene heading. 101 | 102 | 103 | __ 104 | ``` 105 | ^mythic(?:v2)? reset noconfirm$ 106 | ``` 107 | __ 108 | ```js 109 | const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath; 110 | 111 | // Reset the state 112 | confirmObjectPath("_inlineScripts.state.sessionState.mythicv2"); 113 | _inlineScripts.state.sessionState.mythicv2.chaos = 4; 114 | _inlineScripts.state.sessionState.mythicv2.scene = 1; 115 | 116 | // Reset the lists: pcs, npcs, threads 117 | confirmObjectPath("_inlineScripts.state.sessionState.lists"); 118 | _inlineScripts.state.sessionState.lists.pcs = { type: "basic", content: [] }; 119 | _inlineScripts.state.sessionState.lists.npcs = { type: "basic", content: [] }; 120 | _inlineScripts.state.sessionState.lists.threads = { type: "basic", content: [] }; 121 | 122 | // Make sure the session-specific state is still setup 123 | confirmObjectPath("_inlineScripts.mythicv2.details", []); 124 | 125 | // Return a heading for scene 1 126 | return "***\n\n\n### SCENE " + 127 | _inlineScripts.state.sessionState.mythicv2.scene + "\n- Setup:\n - "; 128 | ``` 129 | __ 130 | hidden - No-confirm reset 131 | 132 | 133 | __ 134 | ``` 135 | ^mythicv2 details ?(|[y|n])$ 136 | ``` 137 | __ 138 | ```js 139 | // If the parameter is set, update the flag 140 | if ($1) 141 | { 142 | _inlineScripts.state.sessionState.mythicv2.showDetails = ($1 === "y"); 143 | } 144 | 145 | // Return the current flag state 146 | return expFormat( 147 | "Mythic details are " + (_inlineScripts.state.sessionState.mythicv2.showDetails ? 148 | "__enabled__" : "__disabled__") + "."); 149 | ``` 150 | __ 151 | mythicv2 details {state: y OR n, default: ""} - If {state} is given, assigns it to the mythicv2 "details" mode. Otherwise, displays the current "details" mode. 152 | *** 153 | 154 | __ 155 | __ 156 | ```js 157 | // Make a roll from 1 to max. Store the result as a detail with the name parameter 158 | function roll(name, max) 159 | { 160 | return addDetails(name, Math.trunc(Math.random() * max + 1)); 161 | } 162 | 163 | // Pick an item from array a. Store the result as a detail with the name parameter 164 | function aPick(name, a) { return a[roll(name, a.length)-1]; } 165 | 166 | // Pick an item from array a, weighted by element wIndex of the item. If theRoll is 167 | // passed, use that as the roll. Store the result as a detail with the name parameter 168 | function aPickWeight(name, a, wIndex, theRoll) 169 | { 170 | wIndex = wIndex || 1; 171 | theRoll = theRoll || roll("",a.last()[wIndex]); 172 | addDetails(name, theRoll); 173 | for (const item of a) 174 | { 175 | if (item[wIndex] >= theRoll) 176 | { 177 | return item; 178 | } 179 | } 180 | return a.last(); 181 | } 182 | 183 | // Takes a variable # of arguments. Arguments are grouped into pairs. The first 184 | // item of the pair is a roll's name. The secondis the roll's value. 185 | function addDetails() 186 | { 187 | for (let i = 0; i < arguments.length; i+= 2) 188 | { 189 | if (!arguments[i]) { continue; } 190 | _inlineScripts.mythicv2.details.push(arguments[i] + "=" + arguments[i+1]); 191 | } 192 | return arguments[arguments.length - 1]; 193 | } 194 | 195 | // Return a string representing the detail data gathered for the current action up to 196 | // this point. Each data point is a roll. If the flag is off, this returns "". 197 | function getDetails() 198 | { 199 | if (!_inlineScripts.state.sessionState.mythicv2.showDetails || 200 | !_inlineScripts.mythicv2.details.length) 201 | { 202 | return ""; 203 | } 204 | return "\n- _" + _inlineScripts.mythicv2.details.join(" ") + "_"; 205 | } 206 | 207 | // Reset the details if the current shortcut is user-triggered (i.e. a new action) 208 | function clearDetailsIfUserTriggered() 209 | { 210 | if (expansionInfo.isUserTriggered) 211 | { 212 | _inlineScripts.mythicv2.details = []; 213 | } 214 | } 215 | 216 | // The chaos adjustment modifier is used in a few ways. This lets it's calculation 217 | // be simple to trigger and use. 218 | function getChaosAdjust(multiplier) 219 | { 220 | const chaos = Number(_inlineScripts.state.sessionState.mythicv2.chaos); 221 | const result = 222 | ( (chaos === 3) ? 2 : (chaos === 6) ? -2 : 0 ) * (multiplier || 1); 223 | return addDetails("chaosAdjust", result); 224 | } 225 | ``` 226 | __ 227 | Helper scripts 228 | 229 | 230 | __ 231 | ``` 232 | ^fate ?(|-4|-3|-2|-1|0|1|2|3|4) ?(|y|n)$ 233 | ``` 234 | __ 235 | ```js 236 | // The parameter defaults to 0 237 | $1 = Number($1) || 0; 238 | 239 | // Start a new details list 240 | clearDetailsIfUserTriggered(); 241 | 242 | // Data 243 | const ODDS = ["impossible","no way","very unlikely","unlikely","50/50","likely","very likely","sure thing","has to be"]; 244 | 245 | // Get the wanted flag from the parameter 246 | let wanted = ($2==="n" ? false : true); 247 | 248 | // Make the critical rolls 249 | let fateRoll1 = roll("fateRoll1", 10); 250 | let fateRoll2 = roll("fateRoll2", 10); 251 | let chaosRoll = roll("chaosRoll", 10); 252 | 253 | // calculate the final roll result 254 | let result = 255 | fateRoll1 + fateRoll2 + addDetails("oddsAdjust", ($1) * 2) + 256 | getChaosAdjust(wanted ? 1 : -1); 257 | result = result > 10 ? "YES" : "NO"; 258 | 259 | // The chaos roll triggered. Is it an extreme result, random event or both? 260 | let isChaotic = (chaosRoll <= _inlineScripts.state.sessionState.mythicv2.chaos); 261 | let isExtreme = isChaotic && !!(fateRoll1 % 2) && !!(fateRoll2 % 2); 262 | let isEvent = isChaotic && !(fateRoll1 % 2) && !(fateRoll2 % 2); 263 | if (isChaotic && fateRoll1 === fateRoll2) isExtreme = isEvent = true; 264 | 265 | // If extreme, add such to the result 266 | result = (isExtreme ? "EXTREME " : "") + result; 267 | 268 | // If random event, roll it and record the result 269 | let evtText = isEvent ? ( "\nevent - " + expand("event")[1] ) : ""; 270 | 271 | // Return the roll result, along with any random event result 272 | return expFormat( 273 | "Fate check (" + ODDS[$1 + 4] + "):\n" + result + evtText + getDetails()); 274 | ``` 275 | __ 276 | fate {odds: -4 TO 4 ("impossible" TO "has to be"), default: 0 ("50/50")} {wanted: y OR n, default: y} - Makes a fate check based on {odds}. 277 | This fate check is also based on {wanted}: the desired outcome. Used for the direction of the chaos modifier. 278 | 279 | 280 | __ 281 | ``` 282 | ^fate ?(.*)$ 283 | ``` 284 | __ 285 | ```js 286 | // Parameter is case-insensitive 287 | $1 = $1.trim().toLowerCase(); 288 | 289 | // Start a new details list 290 | clearDetailsIfUserTriggered(); 291 | 292 | // Data 293 | const INPUT_ODDS = 294 | { 295 | "impossible": -4, "no way": -3, "very unlikely": -2, "unlikely": -1, "50/50": 0, "likely": 1, "very likely": 2, "sure thing": 3, "has to be": 4 296 | }; 297 | 298 | // Pull the wanted flag from the parameter 299 | let wanted = " y"; 300 | if ($1.endsWith(" y")) 301 | { 302 | $1 = $1.slice(0,-2); 303 | } 304 | else if ($1.endsWith(" n")) 305 | { 306 | wanted = " n"; 307 | $1 = $1.slice(0,-2); 308 | } 309 | 310 | // Work out the input odds number by comparing the parameter to the data table 311 | const inputOdds = INPUT_ODDS[$1.trim()] || 0; 312 | 313 | // Rely on the other "fate" shortcut to finish the job 314 | return expand("fate " + inputOdds + wanted); 315 | ``` 316 | __ 317 | fate {odds: text, default: "50/50"} {wanted: y OR n, default: y} - Makes a fate check based on {odds}: a specific text such as "impossible", "sure thing", etc. 318 | This fate check is also based on {wanted}: the desired outcome. Used for the direction of the chaos modifier. 319 | 320 | 321 | __ 322 | ``` 323 | ^detail$ 324 | ``` 325 | __ 326 | ```js 327 | // Start a new details list 328 | clearDetailsIfUserTriggered(); 329 | 330 | // Data 331 | let outcomes = [ ["ANGER",4],["SADNESS",5],["FEAR",6],["THREAD NEGATIVE",7,"threads"],["PC NEGATIVE",8,"pcs"],["FOCUS NPC",9,"npcs"],["NPC POSITIVE",10,"npcs"],["FOCUS PC",11,"pcs"],["NPC NEGATIVE",12,"npcs"],["FOCUS THREAD",13,"threads"],["PC POSITIVE",14,"pcs"],["THREAD POSITIVE",15,"threads"],["COURAGE",16],["HAPPINESS",17],["CALM",99] ]; 332 | 333 | // Make the detail roll 334 | let result = roll("roll1", 10) + roll("roll2", 10) + getChaosAdjust(); 335 | 336 | // Pick from the weighted data table based on the detail roll 337 | result = aPickWeight("", outcomes, 1, result); 338 | 339 | // If the result references one of the lists, roll on the list and record the result 340 | let focus = result[2] ? expand("lists pick " + result[2]) : ""; 341 | focus = (focus.length < 2) ? "" : (" (" + focus[1] + ")"); 342 | 343 | // Return the detail roll, along with the item of the list, if applicable 344 | return expFormat("Detail:\n" + result[0] + focus + getDetails()); 345 | ``` 346 | __ 347 | detail - Makes a detail check. 348 | 349 | 350 | __ 351 | ``` 352 | ^event$ 353 | ``` 354 | __ 355 | ```js 356 | // Start a new details list 357 | clearDetailsIfUserTriggered(); 358 | 359 | // Data 360 | let outcomes = [ ["REMOTE",7],["NPC ACTS",28,"npcs"],["NEW NPC",35,null,true],["THREAD ADVANCE",45,"threads"],["THREAD LOSS",52,"threads"],["THREAD END",55,"threads"],["PC NEGATIVE",67,"pcs"],["PC POSITIVE",75,"pcs"],["AMBIGUOUS",83],["NPC NEGATIVE",92,"npcs"],["NPC POSITIVE",100,"npcs"] ]; 361 | 362 | // Get an item from the weighted data table 363 | let result = aPickWeight("eventRoll", outcomes); 364 | 365 | // If the result references one of the lists, roll on the list and record the result 366 | let focus = result[2] ? expand("lists pick " + result[2]) : ""; 367 | focus = (focus.length < 2) ? "" : (" (" + focus[1] + ")"); 368 | 369 | // Make and record a meaning roll 370 | let meaning = expand("meaning " + (result[3] ? "description" : "action"))[1]; 371 | 372 | // Return the event roll result, along with the item of the list, if applicable 373 | return expFormat( 374 | [ "Event:\n", result[0] + focus + " - " + meaning, getDetails(), "" ]); 375 | ``` 376 | __ 377 | event - Makes an event check. 378 | *** 379 | 380 | 381 | __ 382 | ``` 383 | ^meaning(?:| action)$ 384 | ``` 385 | __ 386 | ```js 387 | // Start a new details list 388 | clearDetailsIfUserTriggered(); 389 | 390 | // Data 391 | let value1 = ["ATTAINMENT","STARTING","NEGLECT","FIGHT","RECRUIT","TRIUMPH","VIOLATE","OPPOSE","MALICE","COMMUNICATE","PERSECUTE","INCREASE","DECREASE","ABANDON","GRATIFY","INQUIRE","ANTAGONISE","MOVE","WASTE","TRUCE","RELEASE","BEFRIEND","JUDGE","DESERT","DOMINATE","PROCRASTINATE","PRAISE","SEPARATE","TAKE","BREAK","HEAL","DELAY","STOP","LIE","RETURN","IMMITATE","STRUGGLE","INFORM","BESTOW","POSTPONE","EXPOSE","HAGGLE","IMPRISON","RELEASE","CELEBRATE","DEVELOP","TRAVEL","BLOCK","HARM","DEBASE","OVERINDULGE","ADJOURN","ADVERSITY","KILL","DISRUPT","USURP","CREATE","BETRAY","AGREE","ABUSE","OPPRESS","INSPECT","AMBUSH","SPY","ATTACH","CARRY","OPEN","CARELESSNESS","RUIN","EXTRAVAGANCE","TRICK","ARRIVE","PROPOSE","DIVIDE","REFUSE","MISTRUST","DECEIVE","CRUELTY","INTOLERANCE","TRUST","EXCITEMENT","ACTIVITY","ASSIST","CARE","NEGLIGENCE","PASSION","WORK_HARD","CONTROL","ATTRACT","FAILURE","PURSUE","VENGEANCE","PROCEEDINGS","DISPUTE","PUNISH","GUIDE","TRANSFORM","OVERTHROW","OPPRESS","CHANGE"]; 392 | let value2 = ["GOALS","DREAMS","ENVIRONMENT","OUTSIDE","INSIDE","REALITY","ALLIES","ENEMIES","EVIL","GOOD","EMOTIONS","OPPOSITION","WAR","PEACE","THE_INNOCENT","LOVE","THE_SPIRITUAL","THE_INTELLECTUAL","NEW_IDEAS","JOY","MESSAGES","ENERGY","BALANCE","TENSION","FRIENDSHIP","THE_PHYSICAL","A_PROJECT","PLEASURES","PAIN","POSSESSIONS","BENEFITS","PLANS","LIES","EXPECTATIONS","LEGAL_MATTERS","BUREAUCRACY","BUSINESS","A_PATH","NEWS","EXTERIOR_FACTORS","ADVICE","A_PLOT","COMPETITION","PRISON","ILLNESS","FOOD","ATTENTION","SUCCESS","FAILURE","TRAVEL","JEALOUSY","DISPUTE","HOME","INVESTMENT","SUFFERING","WISHES","TACTICS","STALEMATE","RANDOMNESS","MISFORTUNE","DEATH","DISRUPTION","POWER","A_BURDEN","INTRIGUES","FEARS","AMBUSH","RUMOR","WOUNDS","EXTRAVAGANCE","A_REPRESENTATIVE","ADVERSITIES","OPULENCE","LIBERTY","MILITARY","THE_MUNDANE","TRIALS","MASSES","VEHICLE","ART","VICTORY","DISPUTE","RICHES","STATUS_QUO","TECHNOLOGY","HOPE","MAGIC","ILLUSIONS","PORTALS","DANGER","WEAPONS","ANIMALS","WEATHER","ELEMENTS","NATURE","THE_PUBLIC","LEADERSHIP","FAME","ANGER","INFORMATION"]; 393 | 394 | // Roll on the meaning tables 395 | let result = aPick("actionRoll1", value1) + " _(of)_ " + aPick("actionRoll2", value2); 396 | 397 | // Return the roll result 398 | return expFormat([ "Meaning (action):\n", result, getDetails(), "" ]); 399 | ``` 400 | __ 401 | meaning action - Rolls on the action meaning tables. 402 | - Alternative: __meaning__ 403 | 404 | 405 | __ 406 | ``` 407 | ^meaning description$ 408 | ``` 409 | __ 410 | ```js 411 | // Start a new details list 412 | clearDetailsIfUserTriggered(); 413 | 414 | // Data 415 | let value1 = ["ABNORMALLY","ADVENTUROUSLY","AGGRESSIVELY","ANGRILY","ANXIOUSLY","AWKWARDLY","BEAUTIFULLY","BLEAKLY","BOLDLY","BRAVELY","BUSILY","CALMLY","CAREFULLY","CARELESSLY","CAUTIOUSLY","CEASELESSLY","CHEERFULLY","COMBATIVELY","COOLLY","CRAZILY","CURIOUSLY","DAINTILY","DANGEROUSLY","DEFIANTLY","DELIBERATELY","DELIGHTFULLY","DIMLY","EFFICIENTLY","ENERGETICALLY","ENORMOUSLY","ENTHUSIASTICALLY","EXCITEDLY","FEARFULLY","FEROCIOUSLY","FIERCELY","FOOLISHLY","FORTUNATELY","FRANTICALLY","FREELY","FRIGHTENINGLY","FULLY","GENEROUSLY","GENTLY","GLADLY","GRACEFULLY","GRATEFULLY","HAPPILY","HASTILY","HEALTHILY","HELPFULLY","HELPLESSLY","HOPELESSLY","INNOCENTLY","INTENSELY","INTERESTINGLY","IRRITATINGLY","JOVIALLY","JOYFULLY","JUDGEMENTALLY","KINDLY","KOOKILY","LAZILY","LIGHTLY","LOOSELY","LOUDLY","LOVINGLY","LOYALLY","MAJESTICALLY","MEANINGFULLY","MECHANICALLY","MISERABLY","MOCKINGLY","MYSTERIOUSLY","NATURALLY","NEATLY","NICELY","ODDLY","OFFENSIVELY","OFFICIALLY","PARTIALLY","PEACEFULLY","PERFECTLY","PLAYFULLY","POLITELY","POSITIVELY","POWERFULLY","QUAINTLY","QUARRELSOMELY","QUIETLY","ROUGHLY","RUDELY","RUTHLESSLY","SLOWLY","SOFTLY","SWIFTLY","THREATENINGLY","VERY","VIOLENTLY","WILDLY","YIELDINGLY"]; 416 | let value2 = ["ABANDONED","ABNORMAL","AMUSING","ANCIENT","AROMATIC","AVERAGE","BEAUTIFUL","BIZARRE","CLASSY","CLEAN","COLD","COLORFUL","CREEPY","CUTE","DAMAGED","DARK","DEFEATED","DELICATE","DELIGHTFUL","DIRTY","DISAGREEABLE","DISGUSTING","DRAB","DRY","DULL","EMPTY","ENORMOUS","EXOTIC","FADED","FAMILIAR","FANCY","FAT","FEEBLE","FEMININE","FESTIVE","FLAWLESS","FRESH","FULL","GLORIOUS","GOOD","GRACEFUL","HARD","HARSH","HEALTHY","HEAVY","HISTORICAL","HORRIBLE","IMPORTANT","INTERESTING","JUVENILE","LACKING","LAME","LARGE","LAVISH","LEAN","LESS","LETHAL","LONELY","LOVELY","MACABRE","MAGNIFICENT","MASCULINE","MATURE","MESSY","MIGHTY","MILITARY","MODERN","EXTRAVAGANT","MUNDANE","MYSTERIOUS","NATURAL","NONDESCRIPT","ODD","PALE","PETITE","POOR","POWERFUL","QUAINT","RARE","REASSURING","REMARKABLE","ROTTEN","ROUGH","RUINED","RUSTIC","SCARY","SIMPLE","SMALL","SMELLY","SMOOTH","SOFT","STRONG","TRANQUIL","UGLY","VALUABLE","WARLIKE","WARM","WATERY","WEAK","YOUNG"]; 417 | 418 | // Roll on the meaning tables 419 | let result = 420 | aPick("descriptorRoll1", value1) + " " + aPick("descriptorRoll2", value2); 421 | 422 | // Return the roll result 423 | return expFormat([ "Meaning (description):\n", result, getDetails(), "" ]); 424 | ``` 425 | __ 426 | meaning description - Rolls on the description meaning tables. 427 | *** 428 | 429 | 430 | __ 431 | ``` 432 | ^scene get$ 433 | ``` 434 | __ 435 | ```js 436 | return expFormat( 437 | "The current scene is " + _inlineScripts.state.sessionState.mythicv2.scene + "."); 438 | ``` 439 | __ 440 | scene get - Shows the current scene. 441 | 442 | 443 | __ 444 | ``` 445 | ^scene (1|-1)$ 446 | ``` 447 | __ 448 | ```js 449 | // Start a new details list 450 | clearDetailsIfUserTriggered(); 451 | 452 | // Adjust the chaos by the parameter 453 | let result = 454 | ($1 === "1") ? expUnformat(expand("chaos++")) : 455 | ($1 === "-1") ? expUnformat(expand("chaos--")) : 456 | "Chaos is unchanged at " + expUnformat(expand("chaos")); 457 | 458 | // Prefix the scene heading with a bunch of space 459 | result += "\n\n\n***\n\n\n"; 460 | 461 | // Advance the scene index 462 | _inlineScripts.state.sessionState.mythicv2.scene++; 463 | 464 | // Add the scene header 465 | result += "### SCENE " + _inlineScripts.state.sessionState.mythicv2.scene; 466 | 467 | // Roll scene check for scene control 468 | let chk = roll("sceneCheck", 10); 469 | if (chk <= _inlineScripts.state.sessionState.mythicv2.chaos) 470 | { 471 | // Odd roll? Modify the scene in some way 472 | if (chk % 2) 473 | { 474 | result += "\n- Scene modified"; 475 | } 476 | // Even Roll? Replace the scene with a random event 477 | else 478 | { 479 | result += "\n- Scene replaced:\n" + " - event - " + expand("event")[1]; 480 | } 481 | } 482 | 483 | // Return the scene header 484 | return result + getDetails() + "\n- setup:\n - "; 485 | ``` 486 | __ 487 | scene {chaos adjust: -1 OR 1} - Shifts the chaos value by {chaosAdjust}, then increments the current scene and run a scene check. 488 | 489 | 490 | __ 491 | ``` 492 | ^chaos$ 493 | ``` 494 | __ 495 | ```js 496 | // Return the current chaos value, wrapped in a nice message 497 | return expFormat( 498 | [ "Chaos is __", _inlineScripts.state.sessionState.mythicv2.chaos, "__." ]); 499 | ``` 500 | __ 501 | chaos - Shows the current chaos value. 502 | 503 | 504 | __ 505 | ``` 506 | ^chaos--$ 507 | ``` 508 | __ 509 | ```js 510 | // Lower the chaos value 511 | _inlineScripts.state.sessionState.mythicv2.chaos--; 512 | 513 | // Floor the chaos value (with expansion notifying user of the flooring) 514 | if (_inlineScripts.state.sessionState.mythicv2.chaos < 3) 515 | { 516 | _inlineScripts.state.sessionState.mythicv2.chaos = 3; 517 | return expFormat("Chaos remains at __3__ (hit minimum)."); 518 | } 519 | 520 | return expFormat( 521 | "Chaos lowered to __" + _inlineScripts.state.sessionState.mythicv2.chaos + "__."); 522 | ``` 523 | __ 524 | chaos-- - Decreases the chaos value by 1 (minimum of 3). 525 | 526 | 527 | __ 528 | ``` 529 | ^chaos\+\+$ 530 | ``` 531 | __ 532 | ```js 533 | // Raise the chaos value 534 | _inlineScripts.state.sessionState.mythicv2.chaos++; 535 | 536 | // Ceiling the chaos value (with expansion notifying user of the ceiling) 537 | if (_inlineScripts.state.sessionState.mythicv2.chaos > 6) 538 | { 539 | _inlineScripts.state.sessionState.mythicv2.chaos = 6; 540 | return expFormat("Chaos remains at __6__ (hit maximum)."); 541 | } 542 | 543 | return expFormat( 544 | "Chaos raised to __" + _inlineScripts.state.sessionState.mythicv2.chaos + "__."); 545 | ``` 546 | __ 547 | chaos++ - Increases the chaos value by 1 (maximum of 6). 548 | 549 | 550 | __ 551 | ``` 552 | ^chaos=([3-6])$ 553 | ``` 554 | __ 555 | ```js 556 | // Explicity set the chaos to the parameter 557 | _inlineScripts.state.sessionState.mythicv2.chaos = $1; 558 | 559 | return expFormat("Chaos set to __" + $1 + "__."); 560 | ``` 561 | __ 562 | chaos={value: 3 TO 6} - Sets the chaos value to {value}, an integer from 3 to 6. 563 | *** 564 | 565 | 566 | __ 567 | ``` 568 | ^descriptors?$ 569 | ``` 570 | __ 571 | ```js 572 | // Start a new details list 573 | clearDetailsIfUserTriggered(); 574 | 575 | // Make all rolls necessary for an NPC descriptor 576 | return expFormat( 577 | "Descriptors:\n- personality:\n - " + expand("meaning description")[1] + 578 | "\n- activity:\n - " + expand("meaning action")[1] + getDetails()); 579 | ``` 580 | __ 581 | descriptors - Generates a personality and activity descriptor for an NPC. 582 | 583 | 584 | __ 585 | ``` 586 | ^disposition (-?[0-3])$ 587 | ``` 588 | __ 589 | ```js 590 | // Start a new details list 591 | clearDetailsIfUserTriggered(); 592 | 593 | // Data 594 | let outcomes = 595 | [ ["PASSIVE (-2)",5],["MODERATE (0)",10],["ACTIVE (+2)",15], 596 | ["AGGRESSIVE (+4)",99] ]; 597 | 598 | // Make the base roll (separated from the descriptor adjust as it's used on its own) 599 | let base = roll("roll1", 10) + roll("roll2", 10); 600 | 601 | // Add the descriptor adjust to the base 602 | let result = base + addDetails("descriptorAdjust", Number($1) * 2); 603 | 604 | // Pick the outcome from the weighted data table based on the resulting roll 605 | result = aPickWeight("", outcomes, 1, result)[0]; 606 | 607 | return expFormat("Disposition:\n. " + result + "\n. BASE = " + base + getDetails()); 608 | ``` 609 | __ 610 | disposition {descriptor count: -3 TO 3} - Rolls for an NPC's disposition, modified by {descriptor count}, which represents the total of the NPC's activated descriptors. 611 | - Example: 2 positively activated descriptors and 1 negatively activated descriptor would make a {descriptor count} of __1+1+(-1) = 1__. 612 | 613 | 614 | __ 615 | ``` 616 | ^disposition (-?[0-3]) ([2-9]|1[0-9]|20)$ 617 | ``` 618 | __ 619 | ```js 620 | // Start a new details list 621 | clearDetailsIfUserTriggered(); 622 | 623 | // Data 624 | let outcomes = [ 625 | ["PASSIVE (-2)",5],["MODERATE (0)",10],["ACTIVE (+2)",15], 626 | ["AGGRESSIVE (+4)",99] ]; 627 | 628 | // In this version, of "disposition", the "base" is passed in. 629 | let result = 630 | addDetails("base", Number($2)) + 631 | addDetails("descriptorAdjust", Number($1) * 2); 632 | result = aPickWeight("", outcomes, 1, result); 633 | 634 | return expFormat("Disposition:\n. " + result[0] + "\n. BASE = " + $2 + getDetails()); 635 | ``` 636 | __ 637 | disposition {descriptorCount: -3 TO 3} {base: 2 TO 20} - Displays the NPC disposition determined by the {base} disposition, modified by {descriptorCount}. {descriptor count} represents the total of the NPC's activated descriptors. 638 | - Example: 2 positively activated descriptors and 1 negatively activated descriptor would make a {descriptor count} of __1+1+(-1) = 1__. 639 | 640 | 641 | __ 642 | ``` 643 | ^action (-2|0|2|4)$ 644 | ``` 645 | __ 646 | ```js 647 | // Start a new details list 648 | clearDetailsIfUserTriggered(); 649 | 650 | // Data 651 | let outcomes1 = [ ["NEW ACTION BASED ON THEME",3],["CONTINUE CURRENT ACTION",5],["CONTINUE CURRENT ACTION, +2 DISPOSITION",6],["CONTINUE CURRENT ACTION, -2 DISPOSITION",7],[false,8,0],[false,9,-4],[false,10,4] ]; 652 | let outcomes2 = [ ["TALK / EXPOSITION",6],["NEUTRAL, DISCONNECTED ACTION",8],["ACTION BENEFITS THE PC",10],["GIVE SOMETHING TO THE PC",11],["TRY TO END ENCOUNTER",12],["CHANGE THEME",13],["CHANGE \"%1\" DESCRIPTOR. IT IS ACTIVATED & DECIDES NEXT ACTION.",14,true],["ACT OUT OF SELF INTEREST",17],["TAKE SOMETHING",18],["HURT THE PC",99] ]; 653 | let descriptors = ["IDENTITY","PERSONALITY","ACTIVITY"]; 654 | 655 | // Roll on outcomes1 table 656 | result = aPickWeight("table1Roll", outcomes1); 657 | 658 | // If roll result is to be passed to outcomes2 table, roll on that 659 | if (!result[0]) 660 | { 661 | // Make the formulated roll necessary for the outcomes2 table 662 | result = 663 | roll("table2Roll1", 10) + roll("table2Roll2", 10) + 664 | addDetails("dispositionAdjust", Number($1)) + 665 | addDetails("tableAdjust", result[2]); 666 | 667 | // Pull the rolled result from outcomes2 table 668 | result = aPickWeight("", outcomes2, 1, result); 669 | 670 | // If the outcomes2 roll results in "CHANGE \"%1\" DESCRIPTOR.", pick a 671 | // descriptor and add it to the output text. 672 | if (result[2]) 673 | { 674 | result[0] = result[0].replace("%1", aPick("descriptorRoll", descriptors)); 675 | } 676 | 677 | // Append a standard text for outcomes2 rolls 678 | result[0] += "\n+2/0/-2 DISPOSITION TO MATCH ACTION."; 679 | } 680 | 681 | return expFormat("Action:\n" + result[0] + getDetails()); 682 | ``` 683 | __ 684 | action {dispositionAdjust: -2 OR 0 OR 2 OR 4} - Makes an NPC behavior check, modified by {dispositionAdjust}: the modifier of the NPC's disposition. 685 | 686 | 687 | __ 688 | ``` 689 | ^lists? pick (|[_a-zA-Z][_a-zA-Z0-9]*)(| [1-9][0-9]*)$ 690 | ``` 691 | __ 692 | ```js 693 | return []; 694 | ``` 695 | __ 696 | hidden - A placeholder shortcut in case the shortcut-file lists.sfile isn't available. 697 | --------------------------------------------------------------------------------