Several updates.

This commit is contained in:
jrmyr 2025-04-30 00:30:34 +00:00
parent b76903f6e0
commit aac6dc4542
6 changed files with 108 additions and 55 deletions

View File

@ -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;
}

View File

@ -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
});
}

View File

@ -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}: <t:${phaseEpoch}:R>`
);
if (verbose) {
await interaction.followUp(`- **Phase**: ${currentPhase}\n- **Status Expiration**: <t:${expiration}:R>\n- **Epoch**: <t:${Math.ceil(hangarSync.epoch / 1000)}:R>\n- **Sync**: <t:${Math.floor(new Date(hangarSync.updated).getTime() / 1000)}:R> 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}**: <t:${phaseEpoch}:R>\n` +
`- **Status Expiration**: <t:${expiration}:R>\n` +
`- **Epoch**: <t:${Math.ceil(hangarSync.epoch / 1000)}:R>\n` +
`- **Sync**: <t:${Math.floor(new Date(hangarSync.updated).getTime() / 1000)}:R> by ${syncName}`
);
// Add additional debug info to logs
client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`);

View File

@ -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` })

View File

@ -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'
},

25
prompts/crowley.txt Normal file
View File

@ -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.”