From aac6dc4542c835d88ce82426b953660e4a440c8d Mon Sep 17 00:00:00 2001 From: jrmyr Date: Wed, 30 Apr 2025 00:30:34 +0000 Subject: [PATCH] Several updates. --- _opt/responses.js | 8 +++-- _opt/responsesQuery.js | 9 ++--- _opt/scExecHangarStatus.js | 36 ++++++++++++++++--- _opt/scorekeeper.js | 73 ++++++++++++++++++-------------------- config.js | 12 +++---- prompts/crowley.txt | 25 +++++++++++++ 6 files changed, 108 insertions(+), 55 deletions(-) create mode 100644 prompts/crowley.txt diff --git a/_opt/responses.js b/_opt/responses.js index f0f8686..27d7120 100644 --- a/_opt/responses.js +++ b/_opt/responses.js @@ -7,7 +7,7 @@ import fs from 'fs/promises'; import path from 'path'; import { OpenAI } from 'openai'; import axios from 'axios'; -import { AttachmentBuilder } from 'discord.js'; +import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; // Discord message max length const MAX_DISCORD_MSG_LENGTH = 2000; @@ -271,11 +271,13 @@ async function onMessage(client, cfg, message) { // Previous response ID for context continuity const prev = client.pb?.cache?.get(key); // Enforce minimum score to use AI responses + // Enforce minimum score to use AI responses, but allow guild admins try { + const isAdmin = message.member?.permissions?.has(PermissionFlagsBits.Administrator); const scoreData = await client.scorekeeper.getScore(message.guild.id, message.author.id); - if (scoreData.totalScore < cfg.minScore) { + if (!isAdmin && scoreData.totalScore < cfg.minScore) { await message.reply( - `You need a score of at least ${cfg.minScore} to use AI responses. Your current score is ${scoreData.totalScore.toFixed(2)}.` + `You need an I/O score of at least ${cfg.minScore} to use AI responses. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.` ); return; } diff --git a/_opt/responsesQuery.js b/_opt/responsesQuery.js index e027920..031aeb6 100644 --- a/_opt/responsesQuery.js +++ b/_opt/responsesQuery.js @@ -3,7 +3,7 @@ * Defines and handles the /query command via the OpenAI Responses API, * including optional image generation function calls. */ -import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js'; +import { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; import fs from 'fs/promises'; import path from 'path'; import axios from 'axios'; @@ -169,12 +169,13 @@ export const commands = [ ), async execute(interaction, client) { const cfg = client.config.responses; - // Enforce minimum score to use /query + // Enforce minimum score to use /query, allow guild admins to bypass try { + const isAdmin = interaction.member?.permissions?.has(PermissionFlagsBits.Administrator); const scoreData = await client.scorekeeper.getScore(interaction.guildId, interaction.user.id); - if (scoreData.totalScore < cfg.minScore) { + if (!isAdmin && scoreData.totalScore < cfg.minScore) { return interaction.reply({ - content: `You need a score of at least ${cfg.minScore} to use /query. Your current score is ${scoreData.totalScore.toFixed(2)}.`, + content: `You need an I/O score of at least ${cfg.minScore} to use /query. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.`, ephemeral: true }); } diff --git a/_opt/scExecHangarStatus.js b/_opt/scExecHangarStatus.js index 2691a5d..1ae1b6f 100644 --- a/_opt/scExecHangarStatus.js +++ b/_opt/scExecHangarStatus.js @@ -280,12 +280,40 @@ export const commands = [ minutesUntilNextPhase = timeUntilNextLight; } - // Calculate a timestamp for Discord's formatting and reply - const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60)); - await interaction.reply(`### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]}`); + // Calculate a timestamp for Discord's formatting and reply + const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60)); + // Determine time to next Lock/Unlock phase for inline display + const isUnlocked = currentPhase === 'Unlocked'; + const label = isUnlocked ? 'Lock' : 'Unlock'; + const minutesToPhase = isUnlocked + ? (turningOffDuration + allOffDuration) - cyclePosition + : cycleDuration - cyclePosition; + const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60)); + // Reply with lights and inline time to phase + await interaction.reply( + `### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]} — Time to ${label}: ` + ); if (verbose) { - await interaction.followUp(`- **Phase**: ${currentPhase}\n- **Status Expiration**: \n- **Epoch**: \n- **Sync**: by <@${hangarSync.userId}>`); + // Replace user mention with displayName for last sync + const syncMember = await interaction.guild.members.fetch(hangarSync.userId).catch(() => null); + const syncName = syncMember ? syncMember.displayName : `<@${hangarSync.userId}>`; + + // Calculate time until next Lock/Unlock phase + const isUnlocked = currentPhase === 'Unlocked'; + const label = isUnlocked ? 'Lock' : 'Unlock'; + const minutesToPhase = isUnlocked + ? (turningOffDuration + allOffDuration) - cyclePosition + : cycleDuration - cyclePosition; + const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60)); + + await interaction.followUp( + `- **Phase**: ${currentPhase}\n` + + `- **Time to ${label}**: \n` + + `- **Status Expiration**: \n` + + `- **Epoch**: \n` + + `- **Sync**: by ${syncName}` + ); // Add additional debug info to logs client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`); diff --git a/_opt/scorekeeper.js b/_opt/scorekeeper.js index 777bcf9..a45db2a 100644 --- a/_opt/scorekeeper.js +++ b/_opt/scorekeeper.js @@ -336,14 +336,15 @@ async function runDecay(client, guildId) { } /** - * Calculate score based on formula + * Calculate score based on formula (no scaling factor) + * Formula: (1 + C*commendationValue - D*citationValue) * (input / (output + baseOutput)) */ function calculateScore(data, baseOutput, commendationValue, citationValue) { - // Score = (1 + (Commendations * CommendationValue) - (Citations * CitationValue)) * (Input / (Output + BaseOutput)) * 100 - const multiplier = 1 + (data.commendations * commendationValue) - (data.citations * citationValue); - const activityScore = data.input / (data.output + baseOutput); + const multiplier = 1 + (data.commendations * commendationValue) - (data.citations * citationValue); + const activityScore = data.input / (data.output + baseOutput); - return multiplier * activityScore * 100; + // Removed aesthetic scaling (×100) for raw score + return multiplier * activityScore; } /** @@ -399,10 +400,10 @@ export const commands = [ { data: new SlashCommandBuilder() .setName('score') - .setDescription('View your score or another user\'s score') + .setDescription('View your I/O score or another user\'s I/O score') .addUserOption(option => option.setName('user') - .setDescription('User to check score for (defaults to you)') + .setDescription('User to check I/O score for (defaults to you)') .setRequired(false) ) .addBooleanOption(option => @@ -413,33 +414,36 @@ export const commands = [ execute: async (interaction, client) => { const targetUser = interaction.options.getUser('user') || interaction.user; - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + // Wrap score retrieval and embed generation in try/catch to handle errors gracefully + try { - try { - const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id); - - const embed = new EmbedBuilder() - .setTitle(`Score for ${targetUser.username}`) + // Fetch score data + const baseOutput = client.config.scorekeeper.baseOutput; + const commendationValue = client.config.scorekeeper.commendationValue; + const citationValue = client.config.scorekeeper.citationValue; + const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id); + const embed = new EmbedBuilder() + .setTitle(`I/O Score for ${(await interaction.guild.members.fetch(targetUser.id).catch(() => null))?.displayName || targetUser.username}`) .setColor(0x00AE86) .setThumbnail(targetUser.displayAvatarURL()) .addFields( - { name: 'Total Score', value: scoreData.totalScore.toFixed(2), inline: false }, - { name: 'Commendations', value: scoreData.commendations.toString(), inline: false }, - { name: 'Citations', value: scoreData.citations.toString(), inline: false }, - { name: 'Input Score', value: scoreData.input.toString(), inline: true }, - { name: 'Output Score', value: scoreData.output.toString(), inline: true } + { name: 'Total Score', value: `**${scoreData.totalScore.toFixed(2)}**`, inline: false }, + { name: 'Commendations', value: `**${scoreData.commendations}** x ${commendationValue}`, inline: false }, + { name: 'Citations', value: `**${scoreData.citations}** x ${citationValue}`, inline: false }, + { name: 'Input Score', value: `**${scoreData.input}**`, inline: true }, + { name: 'Output Score', value: `**${scoreData.output}** + ${baseOutput}`, inline: true } ) .setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() }) .setTimestamp(); - await interaction.reply({ embeds: [embed], ephemeral }); - } catch (error) { - client.logger.error(`Error in score command: ${error.message}`); - await interaction.reply({ - content: 'Failed to retrieve score data.', - ephemeral - }); - } + await interaction.reply({ embeds: [embed], ephemeral }); + } catch (error) { + client.logger.error(`Error in score command: ${error.message}`); + try { + await interaction.reply({ content: 'Failed to retrieve I/O score.', ephemeral }); + } catch {} + } } }, @@ -447,14 +451,7 @@ export const commands = [ { data: new SlashCommandBuilder() .setName('leaderboard') -.setDescription('View the server\'s score leaderboard') - .addIntegerOption(option => - option.setName('limit') - .setDescription('Number of users to show (default: 10, max: 25)') - .setRequired(false) - .setMinValue(1) - .setMaxValue(25) - ) + .setDescription('View the server\'s I/O score leaderboard') .addBooleanOption(option => option.setName('ephemeral') .setDescription('Whether the response should be ephemeral') @@ -465,7 +462,7 @@ export const commands = [ const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; await interaction.deferReply({ ephemeral }); - const limit = interaction.options.getInteger('limit') || 10; + const limit = 10; try { const scores = await client.scorekeeper.getScores(interaction.guildId, limit); @@ -481,13 +478,13 @@ export const commands = [ for (let i = 0; i < scores.length; i++) { const score = scores[i]; const user = await guild.members.fetch(score.userId).catch(() => null); - const username = user ? user.user.username : 'Unknown User'; + const displayName = user ? user.displayName : 'Unknown User'; - leaderboardText += `${i + 1}. **${username}**: ${score.totalScore.toFixed(2)}\n`; + leaderboardText += `${i + 1}. **${displayName}**: ${score.totalScore.toFixed(2)}\n`; } const embed = new EmbedBuilder() - .setTitle(`${guild.name} Score Leaderboard`) + .setTitle('I/O Score Leaderboard') .setColor(0x00AE86) .setDescription(leaderboardText) .setFooter({ text: `Showing top ${scores.length} users` }) diff --git a/config.js b/config.js index 8a35870..e3ae682 100644 --- a/config.js +++ b/config.js @@ -55,7 +55,7 @@ export default { conversationExpiry: 30 * 60 * 1000, minScore: 1.0, tools: { - webSearch: false, + webSearch: true, fileSearch: false, imageGeneration: true, }, @@ -70,7 +70,7 @@ export default { baseOutput: 1000, commendationValue: .25, citationValue: .35, - decay: 90, + decay: 80, schedule: '0 0 * * 0' }, @@ -180,7 +180,7 @@ export default { conversationExpiry: 30 * 60 * 1000, minScore: 1.0, tools: { - webSearch: false, + webSearch: true, fileSearch: false, imageGeneration: true, }, @@ -193,9 +193,9 @@ export default { scorekeeper: { baseOutput: 1000, - commendationValue: 1.0, - citationValue: 1.2, - decay: 90, + commendationValue: 0.25, + citationValue: 0.35, + decay: 80, schedule: '0 0 * * 0' }, diff --git a/prompts/crowley.txt b/prompts/crowley.txt new file mode 100644 index 0000000..22a3219 --- /dev/null +++ b/prompts/crowley.txt @@ -0,0 +1,25 @@ +You are Mr. Crowley + +Role: +Manager of the Continental, an upscale, exclusive hotel catering to a discerning clientele. + +Physical Description: +Impeccably groomed, slender build, sharp, angular features, always dressed in a tailored black suit with a crisp white shirt, black tie, and polished shoes. Silver hair neatly parted, piercing blue eyes, and a calm, measured posture. + +Personality & Mannerisms: +Exudes the poise and precision of an English butler. Speaks in a soft, controlled tone with perfect diction. Movements are deliberate, economical, and elegant. Never rushed, never flustered. Maintains unwavering composure and subtle authority in all situations. + +Demeanor: +Discreet, observant, and unfailingly polite. Anticipates guests’ needs before they are voiced. Uses formal address ("Sir," "Madam") and understated gestures (a slight bow, measured hand motions). Enforces hotel rules with unyielding firmness, yet always couched in courtesy and respect. + +Background: +Born and trained in England, with a background in elite hospitality and personal service to nobility. Well-versed in etiquette, security, and discretion. Has a network of trusted staff and an encyclopedic memory of guests and their preferences. + +Dialogue Style: +Uses formal, polished language. Never interrupts. Responds with succinct, respectful replies, often with a subtle wit. Avoids slang, contractions, or casual language. + +Core Motivations: +Uphold the dignity, security, and reputation of the Continental at all costs. Ensure guests’ comfort and confidentiality. Resolve conflicts discreetly and efficiently. + +Sample Dialogue: +“Welcome to the Continental, Sir. Your suite has been prepared to your specifications. Should you require anything further, please do not hesitate to summon me. I trust your stay will be most agreeable.”