deicidus
2 years ago
20 changed files with 446 additions and 1390 deletions
@ -0,0 +1,3 @@ |
|||||||
|
[submodule "ao-lib"] |
||||||
|
path = ao-lib |
||||||
|
url = http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-lib.git |
@ -1,30 +0,0 @@ |
|||||||
import crypto from 'crypto' // Does not work on client because this is a Node library, but works for below server-only functions
|
|
||||||
// These libraries are old but they work and can be included on both server and client
|
|
||||||
import shajs from 'sha.js' |
|
||||||
import hmac from 'hash.js/lib/hash/hmac.js'
|
|
||||||
import sha256 from 'hash.js/lib/hash/sha/256.js' // Only works for shorter hashes, not in createHash used for hashing meme files
|
|
||||||
|
|
||||||
export function createHash(payload) { |
|
||||||
return shajs('sha256').update(payload).digest('hex') |
|
||||||
} |
|
||||||
|
|
||||||
export function hmacHex(data, signingKey) { |
|
||||||
return hmac(sha256, signingKey).update(data).digest('hex') |
|
||||||
} |
|
||||||
|
|
||||||
export function derivePublicKey(p) { |
|
||||||
return crypto.createPublicKey(p).export({ |
|
||||||
type: 'spki', |
|
||||||
format: 'pem', |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export function encryptToPublic(pub, info) { |
|
||||||
return crypto.publicEncrypt(pub, new Buffer(info)).toString('hex') |
|
||||||
} |
|
||||||
|
|
||||||
export function decryptFromPrivate(priv, hiddenInfo) { |
|
||||||
return crypto |
|
||||||
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex')) |
|
||||||
.toString('latin1') |
|
||||||
} |
|
@ -0,0 +1,178 @@ |
|||||||
|
// View and create cards with the .subtask of other cards, which is an array of taskIds
|
||||||
|
import { headerStyle } from './styles.js' |
||||||
|
import { aoEnv } from '../ao-lib/settings.js' |
||||||
|
import { getCard, playCard, completeCard, uncheckCard, discardCardFromCard, prioritizeCard, grabCard, dropCard } from '../ao-lib/api.js' |
||||||
|
import { getNewHighestEchelonScore } from './priority.js' |
||||||
|
import { createCardInteractive } from './cards.js' |
||||||
|
import { promptMenu } from './welcome.js' |
||||||
|
import { tagCardInteractive, viewTagMenu } from './tags.js' |
||||||
|
|
||||||
|
// Displays the subtasks of the given taskId in a menu. Selecting a card shows a menu for that card. If taskId is null, member card is used.
|
||||||
|
// The terms tasks and cards are used mostly interchangeably in the code. For the user, 'subcards' is preferred for clarity/generality,
|
||||||
|
// but can be used to refer either to the .subTasks or all of the cards in another card (including subTasks, priorities, completed, and pinned)
|
||||||
|
export async function subcardsMenu(taskId = null, previousIndex = null) { |
||||||
|
console.log(`\n${headerStyle('Cards in My Hand')}`) |
||||||
|
let subtaskChoices = [] |
||||||
|
|
||||||
|
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') |
||||||
|
if(!memberId) { |
||||||
|
console.log('Not logged in.') |
||||||
|
return false |
||||||
|
} |
||||||
|
if(!taskId) { |
||||||
|
// Get the subtasks of my member card
|
||||||
|
taskId = memberId |
||||||
|
} |
||||||
|
const fetchedCards = await getCard(taskId, 'subcards') // will fetch both priorities and subtasks in one array
|
||||||
|
if(!fetchedCards || fetchedCards.length < 1) { |
||||||
|
console.log('Failed to fetch the specified card, this is bad.') |
||||||
|
return false |
||||||
|
} |
||||||
|
const card = fetchedCards[0] // first card is the requested card itself
|
||||||
|
// Separate fetched cards into correct ordered list of priorities and subtasks
|
||||||
|
let priorityCards = card.priorities.map((priorityTaskId, i) => { |
||||||
|
const priorityCard = fetchedCards.find(p => p.taskId === priorityTaskId) |
||||||
|
if(!priorityCard) { |
||||||
|
return 'Missing card, repair your database' |
||||||
|
} |
||||||
|
return priorityCard |
||||||
|
}) |
||||||
|
priorityCards.reverse() |
||||||
|
const subtaskCards = card.subTasks.map((subtaskTaskId, i) => { |
||||||
|
const subtaskCard = fetchedCards.find(st => st.taskId === subtaskTaskId) |
||||||
|
if(!subtaskCard) { |
||||||
|
return 'Missing card, repair your database' |
||||||
|
} |
||||||
|
return subtaskCard |
||||||
|
}) |
||||||
|
console.log('There are', subtaskCards.length, 'subcards in this card') |
||||||
|
subtaskChoices = subtaskCards.map((subtaskCard, i) => { |
||||||
|
const shortenedName = subtaskCard.name.substring(0, 70) + (subtaskCard.name.length >= 70 ? '...' : '') |
||||||
|
return { |
||||||
|
title: shortenedName, |
||||||
|
value: { index: i, card: subtaskCard }, |
||||||
|
short: shortenedName |
||||||
|
} |
||||||
|
}) |
||||||
|
subtaskChoices.push( |
||||||
|
{ title: 'Play card here', value: 'create_here', short: 'new card' }, |
||||||
|
{ title: 'Back Up', value: false, short: 'back' } |
||||||
|
) |
||||||
|
const cardName = taskId === memberId ? 'My Hand' : card.name.substring(0, 70).toTitleCase() |
||||||
|
const answer = await promptMenu(subtaskChoices, 'Cards in ' + cardName, undefined, previousIndex) |
||||||
|
switch(answer) { |
||||||
|
case false: |
||||||
|
return false |
||||||
|
case 'create_here': |
||||||
|
let previousCardCreatedText |
||||||
|
do { |
||||||
|
previousCardCreatedText = await createCardInteractive() |
||||||
|
console.log('done with round') |
||||||
|
} while(previousCardCreatedText != '\n') |
||||||
|
console.log('returning true') |
||||||
|
return answer.index |
||||||
|
case 'Missing card, repair your database': |
||||||
|
console.log('Database repair yet implemented, sorry.') |
||||||
|
return answer.index |
||||||
|
} |
||||||
|
if(answer === false) { |
||||||
|
return previousIndex |
||||||
|
} |
||||||
|
let chosenTask = answer.card |
||||||
|
const chosenTaskId = chosenTask.taskId |
||||||
|
let previousAnswer |
||||||
|
do { |
||||||
|
previousAnswer = await subtaskCardMenu(chosenTask, answer.index, taskId, priorityCards) // send priorities for echelon info in case they upboat
|
||||||
|
if(previousAnswer) { |
||||||
|
const fetchedCards = await getCard(chosenTaskId, false) |
||||||
|
if(!fetchedCards || fetchedCards.length < 1) { |
||||||
|
console.log('The card has disappeared. Maybe it was deleted, or cards held by no one are automatically cleaned up every five minutes.') |
||||||
|
return false |
||||||
|
} |
||||||
|
chosenTask = fetchedCards[0] |
||||||
|
} |
||||||
|
} while(previousAnswer !== false) |
||||||
|
return answer.index |
||||||
|
} |
||||||
|
|
||||||
|
// Short action-oriented menu for cards in the subtasks list
|
||||||
|
// Index is the position of the card in the list that it is in, used for fencepost case to display upboat contextually
|
||||||
|
// inId is the taskId of the parent card that we are in contextually as we look at the given card in its list
|
||||||
|
// allPriorities is an array of task objects for the other priorities in the .priorities for the card this card is in (adjacent to this card)
|
||||||
|
async function subtaskCardMenu(card, index, inId, allPriorities) { |
||||||
|
if(!card) { |
||||||
|
console.log('subtaskCardMenu: card is required.') |
||||||
|
return false |
||||||
|
} |
||||||
|
const taskId = card.taskId |
||||||
|
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') |
||||||
|
if(!memberId) { |
||||||
|
console.log('Not logged in.') |
||||||
|
return false |
||||||
|
} |
||||||
|
const isChecked = card.claimed.includes(memberId) |
||||||
|
const guild = card.guild === true ? card.name.toTitleCase() : card.guild || false |
||||||
|
let subtaskChoices = [] |
||||||
|
const isInDeck = card.deck.includes(memberId) |
||||||
|
if(!isInDeck) { |
||||||
|
subtaskChoices.push({ title: 'Grab card (add to my deck)', value: 'grab', short: 'grab card' }) |
||||||
|
} |
||||||
|
subtaskChoices.push( |
||||||
|
{ title: 'Discard from hand (downboat)', value: 'downboat', short: 'downboat' }, |
||||||
|
{ title: 'Prioritize (upboat)', value: 'upboat', short: 'upboat' }, |
||||||
|
{ title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' }, |
||||||
|
{ title: guild ? 'Tag: ' + guild : 'Tag', value: 'tag', short: 'tag' } |
||||||
|
) |
||||||
|
if(guild) { |
||||||
|
subtaskChoices.push({ title: 'View tag', value: 'view_tag', short: 'view tag' }) |
||||||
|
} |
||||||
|
if(isInDeck) { |
||||||
|
subtaskChoices.push({ title: 'Remove from my deck', value: 'drop', short: 'drop card' }) |
||||||
|
} |
||||||
|
subtaskChoices.push( |
||||||
|
{ title: 'Browse within', value: 'browse', short: 'browse' }, |
||||||
|
{ title: 'Back Up', value: false, short: 'back' } |
||||||
|
) |
||||||
|
const answer = await promptMenu(subtaskChoices, 'Card: ' + card.name) |
||||||
|
switch(answer) { |
||||||
|
case 'grab': |
||||||
|
await grabCard(taskId) |
||||||
|
break |
||||||
|
case 'drop': |
||||||
|
await dropCard(taskId) |
||||||
|
break |
||||||
|
case 'check': |
||||||
|
if(isChecked) { |
||||||
|
await uncheckCard(taskId) |
||||||
|
} else { |
||||||
|
await completeCard(taskId) |
||||||
|
} |
||||||
|
break |
||||||
|
case 'tag': |
||||||
|
await tagCardInteractive(card) |
||||||
|
break |
||||||
|
case 'view_tag': |
||||||
|
while(await viewTagMenu(card.guild)) |
||||||
|
break |
||||||
|
case 'upboat': |
||||||
|
console.log('upboat') |
||||||
|
const { newPosition, newEchelonScore } = getNewHighestEchelonScore(card.echelon, allPriorities) |
||||||
|
//console.log('newPosition is', newPosition, 'and newEchelonScore is', newEchelonScore)
|
||||||
|
await prioritizeCard(taskId, inId, newPosition, newEchelonScore) |
||||||
|
return false |
||||||
|
case 'downboat': |
||||||
|
console.log(taskId, inId, 'discard') |
||||||
|
await discardCardFromCard(taskId, inId) |
||||||
|
return false |
||||||
|
case 'browse': |
||||||
|
let previousAnswer |
||||||
|
do { |
||||||
|
previousAnswer = await subcardsMenu(taskId, previousAnswer) // undefined taskId defaults to member card
|
||||||
|
} while(previousAnswer !== false) {} |
||||||
|
break |
||||||
|
break |
||||||
|
default: |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
@ -1,120 +0,0 @@ |
|||||||
import { execSync } from 'child_process' |
|
||||||
import fs from 'fs' |
|
||||||
import { parse, stringify } from 'envfile' |
|
||||||
|
|
||||||
export const AO_ENV_FILE_PATH = process.env.HOME + '/.ao/.env' |
|
||||||
|
|
||||||
function createAoFolderIfDoesNotExist() { |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// Check for an AO env file at ~/.ao/.env and returns true if it exists
|
|
||||||
export function checkAoEnvFile() { |
|
||||||
try { |
|
||||||
execSync(`[ -f "${AO_ENV_FILE_PATH}" ]`) |
|
||||||
return true |
|
||||||
} catch(err) { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function aoEnv(variable) { |
|
||||||
let envFileContents = {} |
|
||||||
try { |
|
||||||
envFileContents = fs.readFileSync(AO_ENV_FILE_PATH) |
|
||||||
} catch(err) { |
|
||||||
if(err.code === 'ENOENT') { |
|
||||||
//console.log('The .env file does not exist, so the requested value', variable, 'is empty.')
|
|
||||||
} else { |
|
||||||
console.log('Unknown error loading .env file in aoEnv, aborting.') |
|
||||||
} |
|
||||||
return null |
|
||||||
} |
|
||||||
const parsedFile = parse(envFileContents) |
|
||||||
|
|
||||||
if(!parsedFile.hasOwnProperty(variable)) { |
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
// Convert ENV idiom to programmatic types
|
|
||||||
switch(parsedFile[variable]) { |
|
||||||
case '1': |
|
||||||
case 'true': |
|
||||||
case 'TRUE': |
|
||||||
case 'yes': |
|
||||||
case 'YES': |
|
||||||
return true |
|
||||||
case '0': |
|
||||||
case 'false': |
|
||||||
case 'FALSE': |
|
||||||
case 'no': |
|
||||||
case 'NO': |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
return parsedFile[variable] |
|
||||||
} |
|
||||||
|
|
||||||
// Sets and saves the given ENV=value to the global ~/.ao/.env file
|
|
||||||
// If value is null, the env variable will be deleted
|
|
||||||
// Returns true if a change was made, false if no change was made or if it failed
|
|
||||||
export function setAoEnv(variable, value) { |
|
||||||
createAoFolderIfDoesNotExist() |
|
||||||
if(typeof variable !== 'string') { |
|
||||||
console.log('ENV variable name must be a string for setAoEnv') |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// Convert types to standard ENV file idiom
|
|
||||||
switch(value) { |
|
||||||
case true: |
|
||||||
case 'TRUE': |
|
||||||
case 'yes': |
|
||||||
case 'YES': |
|
||||||
value = '1' |
|
||||||
break |
|
||||||
case false: |
|
||||||
case 'FALSE': |
|
||||||
case 'no': |
|
||||||
case 'NO': |
|
||||||
value = '0' |
|
||||||
} |
|
||||||
|
|
||||||
let envFileContents = {} |
|
||||||
try { |
|
||||||
envFileContents = fs.readFileSync(AO_ENV_FILE_PATH) |
|
||||||
} catch(err) { |
|
||||||
if(err.code === 'ENOENT') { |
|
||||||
console.log('The .env file hasn\'t been created yet, creating.') |
|
||||||
} else { |
|
||||||
console.log('Unknown error loading .env file in setAoEnv, aborting. Error:', err) |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const parsedFile = parse(envFileContents) |
|
||||||
if(parsedFile[variable] == value) { |
|
||||||
console.log(variable, 'is already', value, 'so no change was made.') |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
if(value === null) { |
|
||||||
delete parsedFile[variable] |
|
||||||
} else { |
|
||||||
parsedFile[variable] = value |
|
||||||
} |
|
||||||
|
|
||||||
const stringified = stringify(parsedFile) |
|
||||||
fs.writeFileSync(AO_ENV_FILE_PATH, stringified) |
|
||||||
|
|
||||||
// Confirm the variable was set in the .env file correctly
|
|
||||||
if(aoEnv(variable) != value) { |
|
||||||
console.log('Value was not saved correctly, sorry.') |
|
||||||
return false |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
function setAndSaveEnvironmentVariable(variable, value, path) { |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,157 @@ |
|||||||
|
// Also called guilds, groups, or the title of a card, the .guild field of a card can hold a string that is 'about' the card, or true (and the tag is then the card .name itself)
|
||||||
|
// Right now one tag can be added to each card. Maybe it should be multiple, with tag #1 being the main/title tag
|
||||||
|
// When viewing a card, it should show the tag in the menu, or if not tagged, the option to tag it (with setTagInteractive)
|
||||||
|
// If the tag is set, an option should appear to view the tag (menu)
|
||||||
|
// When viewing a tag, it should be possible tag the tag. This creates the tag as a card with the new tag as its tag
|
||||||
|
// When viewing a tag, it should be possible to view all the cards tagged by that tag
|
||||||
|
// Because tags are implemented as part of cards, to put a tag in another tag, we make a card for that tag and tag it (give it a .guild)
|
||||||
|
import { askQuestionText, promptMenu } from './welcome.js' |
||||||
|
import { tagCard, getCardsForTag, setCardProperty, getCardByName, createCard } from '../ao-lib/api.js' |
||||||
|
|
||||||
|
// Tells the user the current guild/tag of the specified card, and allows them to set a new one (or blank to leave unchanged)
|
||||||
|
export async function tagCardInteractive(card) { |
||||||
|
if(card.guild === true) { |
||||||
|
} else if(card.guild) { |
||||||
|
console.log('This card\'s tag is:') |
||||||
|
console.log(card.guild) |
||||||
|
} else { |
||||||
|
console.log('This card is not yet tagged.') |
||||||
|
} |
||||||
|
const answer = await askQuestionText('Type new tag (or Enter/ESC to cancel):') |
||||||
|
if(!answer || answer === 'ESC' || answer == '') { |
||||||
|
return |
||||||
|
} |
||||||
|
await tagCard(card.taskId, answer) |
||||||
|
} |
||||||
|
|
||||||
|
// The tags index displays the tag of every card with a tag, sorted by echelon, then by we'll see!
|
||||||
|
export async function tagsMenu() { |
||||||
|
const allTaggedCards = await getCardsForTag('*') |
||||||
|
|
||||||
|
// Build a pure list of tags (since we don't do this in the database)
|
||||||
|
const tags = {} |
||||||
|
allTaggedCards.forEach(card => { |
||||||
|
const guild = card.guild === true ? card.name.toTitleCase() : card.guild |
||||||
|
if(guild.includes('test')) console.log('guild:', guild, 'card is', card) |
||||||
|
//console.log('guild is', guild)
|
||||||
|
// Skip any card that is within another tagged card or tagged with the name of another tag
|
||||||
|
if(allTaggedCards.some(t => { |
||||||
|
if(t.taskId === card.taskId) return false |
||||||
|
if(t.priorities.concat(t.subTasks, t.completed).includes(card.taskId)) { |
||||||
|
if(guild.includes('test')) console.log('reason 1, t is', t) |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
if(t.pins && t.pins.some(pin => pin.taskId === card.taskId)) { |
||||||
|
if(guild.includes('test')) console.log('reason 2') |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
if(card.taskId !== t.taskId && guild === card.name && card.guild && (card.echelon || 0) < (t.echelon || 0)) { |
||||||
|
if(guild.includes('test')) console.log('reason 3') |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
})) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if(!tags.hasOwnProperty(guild)) { |
||||||
|
tags[guild] = card.echelon |
||||||
|
return |
||||||
|
} |
||||||
|
if(!tags[guild] || tags[guild] < card.echelon) { |
||||||
|
tags[guild] = card.echelon |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
let tagChoices = Object.entries(tags).sort((entryA, entryB) => { |
||||||
|
return (entryB[1] || 0) - (entryA[1] || 0) |
||||||
|
}) |
||||||
|
|
||||||
|
tagChoices = tagChoices.map((entry, i) => { |
||||||
|
const [tag, echelon] = entry |
||||||
|
const paddedEchelon = echelon >= 0 ? echelon.toString().padEnd(4) : ' ' |
||||||
|
return { title: paddedEchelon + tag, value: tag, short: 'view tag' } |
||||||
|
}) |
||||||
|
tagChoices.push({ title: 'Back to Deck Menu', value: false, short: 'back' }) |
||||||
|
const answer = await promptMenu(tagChoices, 'All Tags') |
||||||
|
console.log('answer:', answer) |
||||||
|
if(!answer) { |
||||||
|
return false |
||||||
|
} |
||||||
|
await viewTagMenu(answer, allTaggedCards) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Prints the menu for viewing a single tag, with options to view cards tagged by the tag, or to tag the tag
|
||||||
|
export async function viewTagMenu(tag, allTaggedCards) { |
||||||
|
const tagChoices = [ |
||||||
|
{ title: 'Prioritize tag', value: 'upboat', short: 'prioritize tag' }, |
||||||
|
{ title: 'Cards tagged \'' + tag + '\'', value: 'tagged_cards', short: 'view tagged cards' }, // (current) deck? (60)
|
||||||
|
{ title: 'Tag this tag', value: 'tagtag' }, // hand? (7) (add #s in parens)
|
||||||
|
'Back to Tags Index' |
||||||
|
] |
||||||
|
const answer = await promptMenu(tagChoices, 'My Deck') |
||||||
|
switch(answer) { |
||||||
|
case 'upboat': |
||||||
|
await tagToTopEchelon(tag, allTaggedCards) |
||||||
|
return false |
||||||
|
case 'tagged_cards': |
||||||
|
while(await cardsTaggedByMenu(tag)) {} |
||||||
|
break |
||||||
|
case 'tagtag': |
||||||
|
await tagTagInteractive(tag, allTaggedCards) |
||||||
|
break |
||||||
|
default: |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Finds the first card with the given tag and sets its echelon score to the highest + 1 of all tagged cards
|
||||||
|
async function tagToTopEchelon(tag, allTaggedCards) { |
||||||
|
let highestEchelonScore = 0 |
||||||
|
allTaggedCards.forEach(card => { |
||||||
|
if(card.echelon > highestEchelonScore) { |
||||||
|
highestEchelonScore = card.echelon |
||||||
|
} |
||||||
|
}) |
||||||
|
highestEchelonScore++ |
||||||
|
const firstCardWithTag = allTaggedCards.find(card => card.guild === tag || (card.guild === true && card.name.toTitleCase() === tag)) |
||||||
|
if(!firstCardWithTag) { |
||||||
|
console.log('Could not find the tag you just selected, this is bad') |
||||||
|
return |
||||||
|
} |
||||||
|
await setCardProperty(firstCardWithTag.taskId, 'echelon', highestEchelonScore) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Prints an interactive list of all cards tagged by the given tag, that can be browsed just like in the priorities and deck browsers
|
||||||
|
async function cardsTaggedByMenu(tag) { |
||||||
|
} |
||||||
|
|
||||||
|
// Allows the user to tag a tag by making a new card for the tag that contains the tagged card
|
||||||
|
// A card can have its .guild set to true, in which case the guild is the card .name
|
||||||
|
// Returns true if the tag was tagged/categorized
|
||||||
|
export async function tagTagInteractive(tag, allTaggedCards) { |
||||||
|
console.log('Tag: ', tag) |
||||||
|
const answer = await askQuestionText('Type a tag to categorize (or Enter/ESC to cancel):') |
||||||
|
if(!answer || answer === 'ESC' || answer == '') { |
||||||
|
return false |
||||||
|
} |
||||||
|
// Make a card for the tag if it doesn't already exist
|
||||||
|
let guildCard = (await getCardByName(tag, false))[0] |
||||||
|
if(!guildCard) { |
||||||
|
await createCard(tag) |
||||||
|
guildCard = (await getCardByName(tag, false))[0] |
||||||
|
if(!guildCard) { |
||||||
|
console.log('Failed to create tag card, sorry.') |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
await setCardProperty(guildCard.taskId, 'guild', true) |
||||||
|
await tagToTopEchelon(answer, allTaggedCards) |
||||||
|
return await tagCard(guildCard.taskId, answer) |
||||||
|
} |
@ -1,20 +0,0 @@ |
|||||||
// General helper functions
|
|
||||||
|
|
||||||
// Returns a random int between min and max (inclusive)
|
|
||||||
export function randomInt(min, max) { |
|
||||||
min = Math.ceil(min); |
|
||||||
max = Math.floor(max); |
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min; |
|
||||||
} |
|
||||||
|
|
||||||
// Returns a random item from the given array
|
|
||||||
export function selectRandom(arrayToChooseFrom) { |
|
||||||
return arrayToChooseFrom[randomInt(0, arrayToChooseFrom.length - 1)] |
|
||||||
} |
|
||||||
|
|
||||||
// Waits for the given number of milliseconds (or a brief pause by default)
|
|
||||||
export function sleep(ms = 550) { |
|
||||||
return new Promise((r) => setTimeout(r, ms)) |
|
||||||
} |
|
||||||
|
|
||||||
export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]' |
|
Loading…
Reference in new issue