Compare commits
8 Commits
9658dd81ee
...
3b9c66ebfd
Author | SHA1 | Date |
---|---|---|
Jill | 3b9c66ebfd | |
Jill | 3da36de9f6 | |
Jill | eb1dd27d6b | |
Jill | 77dbd8ee3f | |
Jill | c714596653 | |
Jill | 0594cc71db | |
Jill | a2a56f60f1 | |
Jill | 9eecec4894 |
|
@ -1,3 +1,5 @@
|
|||
{
|
||||
"token": "token"
|
||||
"token": "token",
|
||||
"sitePort": 15385,
|
||||
"siteURL": "https://localhost:15385"
|
||||
}
|
|
@ -17,7 +17,7 @@ rest
|
|||
const commandFiles = fs.readdirSync("./dist/commands").filter((file) => file.endsWith(".js") && !file.startsWith('.'));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const command = require(`./dist/commands/${file}`);
|
||||
const command = require(`./dist/commands/${file}`).default;
|
||||
commands.push(command);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.createTable('customCraftingRecipes', (table) => {
|
||||
table.increments('id');
|
||||
table.string('station');
|
||||
})
|
||||
.createTable('customCraftingRecipeItems', (table) => {
|
||||
table.integer('id').references('id').inTable('customCraftingRecipes').notNullable();
|
||||
table.integer('item').notNullable();
|
||||
table.integer('quantity').defaultTo(1);
|
||||
table.enum('type', ['input', 'output', 'requirement']);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.dropTable('customCraftingRecipes')
|
||||
.dropTable('customCraftingRecipes');
|
||||
};
|
|
@ -15,6 +15,7 @@
|
|||
"chalk": "^4.1.2",
|
||||
"d3-array": "^2.12.1",
|
||||
"discord.js": "^14.14.1",
|
||||
"express": "^4.18.2",
|
||||
"got": "^11.8.6",
|
||||
"knex": "^3.0.1",
|
||||
"outdent": "^0.8.0",
|
||||
|
@ -25,6 +26,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/parse-color": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
|
|
2336
pnpm-lock.yaml
2336
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
import { AutocompleteInteraction, GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { CraftingStationCooldown, db } from '../lib/db';
|
||||
import { getStation, canUseStation, craftingStations, verb } from '../lib/rpg/craftingStations';
|
||||
import { getStation, canUseStation, craftingStations, verb, CraftingStation } from '../lib/rpg/craftingStations';
|
||||
import { formatItem, getItemQuantity, formatItems, getMaxStack, giveItem, formatItemsArray } from '../lib/rpg/items';
|
||||
import { getRecipe, defaultRecipes, formatRecipe } from '../lib/rpg/recipes';
|
||||
import { Command } from '../types/index';
|
||||
|
@ -95,15 +95,21 @@ export default {
|
|||
nextUsableAt = Date.now() + station.cooldown * 1000;
|
||||
}
|
||||
|
||||
return interaction.followUp(`${station.emoji} ${verb(station)} ${formatItemsArray(outputs)}!${nextUsableAt ? `\n${station.name} usable again <t:${Math.floor(nextUsableAt / 1000)}:R>` : ''}`);
|
||||
return interaction.followUp(`${station.emoji} ${verb(station)} ${formatItemsArray(outputs)}!${outputs.length === 1 ? `\n_${outputs[0].item.description}_` : ''}${nextUsableAt ? `\n${station.name} usable again <t:${Math.floor(nextUsableAt / 1000)}:R>` : ''}`);
|
||||
},
|
||||
|
||||
autocomplete: async (interaction: AutocompleteInteraction) => {
|
||||
const focused = interaction.options.getFocused(true);
|
||||
|
||||
if (focused.name === 'station') {
|
||||
const found = craftingStations
|
||||
.filter(station => canUseStation(interaction.user.id, station))
|
||||
const found = (await Promise.all(
|
||||
craftingStations
|
||||
.map(async station => [station, await canUseStation(interaction.user.id, station)])
|
||||
))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.filter(([_station, usable]) => usable)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.map(([station, _]) => station as CraftingStation)
|
||||
.filter(station => station.name.toLowerCase().includes(focused.value.toLowerCase()))
|
||||
.map(station => ({
|
||||
name: `${station.emoji} ${station.name}`,
|
||||
|
|
|
@ -24,7 +24,7 @@ export default {
|
|||
const items = (await Promise.all(itemsList.map(async i => ({item: await getItem(i.item), quantity: i.quantity})))).filter(i => i.item);
|
||||
|
||||
await interaction.followUp(
|
||||
`Your inventory:\n${items.length === 0 ? '_Your inventory is empty!_' : items.map(i => `- ${formatItems(i.item!, i.quantity)}`).join('\n')}`
|
||||
`Your inventory:\n${items.length === 0 ? '_Your inventory is empty!_' : items.map(i => `- ${formatItems(i.item!, i.quantity)}\n_${i.item!.description}_`).join('\n')}`
|
||||
);
|
||||
}
|
||||
} satisfies Command;
|
|
@ -0,0 +1,207 @@
|
|||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, ComponentType, Events, ModalBuilder, SlashCommandBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
|
||||
import { Command } from '../types/index';
|
||||
import { Items, getItem } from '../lib/rpg/items';
|
||||
import { formatRecipe } from '../lib/rpg/recipes';
|
||||
import { craftingStations, getStation } from '../lib/rpg/craftingStations';
|
||||
import { CustomCraftingRecipe, CustomCraftingRecipeItem, db } from '../lib/db';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('recipe')
|
||||
.setDescription('[ADMIN] Manage custom recipes for items')
|
||||
.addSubcommand(sub =>
|
||||
sub
|
||||
.setName('create')
|
||||
.setDescription('[ADMIN] Create a custom recipe')
|
||||
)
|
||||
.setDMPermission(false)
|
||||
.setDefaultMemberPermissions(0),
|
||||
|
||||
execute: async (interaction: CommandInteraction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const sub = interaction.options.getSubcommand(true);
|
||||
|
||||
if (sub === 'create') {
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder().setCustomId(`recipe-create-${interaction.guildId}`).setLabel('I\'ve got my string ready!').setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
await interaction.followUp({
|
||||
ephemeral: true,
|
||||
content: `To create a recipe, go here: ${interaction.client.config.siteURL}/create-recipe/?guild=${interaction.guildId}\nOnce done, click the button below and paste the resulting string in.`,
|
||||
components: [row]
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async onClientReady(bot) {
|
||||
bot.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!('customId' in interaction)) return;
|
||||
const id = interaction.customId;
|
||||
if (!id.startsWith('recipe-')) return;
|
||||
if (!interaction.member) return;
|
||||
|
||||
if (id.startsWith('recipe-create-')) {
|
||||
//const guildID = id.split('-')[2];
|
||||
|
||||
if (interaction.isMessageComponent()) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(interaction.customId)
|
||||
.setTitle('Recipe Creator');
|
||||
|
||||
const input = new TextInputBuilder()
|
||||
.setCustomId('recipe-create-textbox')
|
||||
.setLabel('Paste in your recipe string here:')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setRequired(true);
|
||||
|
||||
const row = new ActionRowBuilder<TextInputBuilder>().addComponents(input);
|
||||
modal.addComponents(row);
|
||||
interaction.showModal(modal);
|
||||
} else if (interaction.isModalSubmit()) {
|
||||
const field = interaction.fields.getField('recipe-create-textbox', ComponentType.TextInput);
|
||||
const recipeString = field.value;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = await Promise.all(
|
||||
recipeString
|
||||
.split('|')
|
||||
.map(items =>
|
||||
Promise.all(
|
||||
items
|
||||
.split(';')
|
||||
.map(itemStack =>
|
||||
itemStack.split(',')
|
||||
)
|
||||
.map(async ([itemID, quantity]) => (
|
||||
{
|
||||
item: (await getItem(parseInt(itemID)))!,
|
||||
quantity: parseInt(quantity)
|
||||
}
|
||||
))
|
||||
)
|
||||
)
|
||||
) as Items[][];
|
||||
} catch (err) {
|
||||
await interaction.followUp(`This is not a valid string!: \`${(err as Error).message}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const
|
||||
inputs = parsed[0] || [],
|
||||
requirements = parsed[1] || [],
|
||||
outputs = parsed[2] || [];
|
||||
|
||||
const recipe = {
|
||||
inputs, requirements, outputs,
|
||||
station: 'hands',
|
||||
id: 0
|
||||
};
|
||||
|
||||
const components = [
|
||||
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.addOptions(
|
||||
...craftingStations
|
||||
.map(station => ({
|
||||
label: `${station.emoji} ${station.name}`,
|
||||
value: station.key,
|
||||
description: station.description
|
||||
}))
|
||||
)
|
||||
.setMinValues(1)
|
||||
.setMaxValues(1)
|
||||
.setCustomId('recipe-select-station')
|
||||
),
|
||||
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('recipe-select-done')
|
||||
.setLabel('Done')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(true)
|
||||
)
|
||||
];
|
||||
|
||||
const msg = await interaction.followUp({
|
||||
content: `${formatRecipe(recipe)}\n_Select a crafting station, and you're good to go!_`,
|
||||
components: components,
|
||||
fetchReply: true
|
||||
});
|
||||
|
||||
const selectCollector = msg.createMessageComponentCollector({
|
||||
componentType: ComponentType.StringSelect,
|
||||
time: 60_000 * 5,
|
||||
});
|
||||
|
||||
selectCollector.on('collect', selectInteraction => {
|
||||
const newStation = selectInteraction.values[0];
|
||||
recipe.station = newStation;
|
||||
components[1].components[0].setDisabled(false);
|
||||
interaction.editReply({
|
||||
content: `${formatRecipe(recipe)}\n_Select a crafting station, and you're good to go!_`,
|
||||
components: components
|
||||
});
|
||||
const station = getStation(newStation);
|
||||
selectInteraction.reply({
|
||||
content: `Set station to ${station?.emoji} **${station?.name}**`,
|
||||
ephemeral: true
|
||||
});
|
||||
});
|
||||
selectCollector.on('end', () => {
|
||||
interaction.editReply({
|
||||
content: msg.content,
|
||||
components: []
|
||||
});
|
||||
});
|
||||
|
||||
const buttonInteraction = await msg.awaitMessageComponent({ componentType: ComponentType.Button, time: 60_000 * 5 });
|
||||
selectCollector.stop();
|
||||
|
||||
const [customRecipe] = await db<CustomCraftingRecipe>('customCraftingRecipes')
|
||||
.insert({
|
||||
station: recipe.station
|
||||
})
|
||||
.returning('id');
|
||||
|
||||
for (const input of recipe.inputs) {
|
||||
await db<CustomCraftingRecipeItem>('customCraftingRecipeItems')
|
||||
.insert({
|
||||
id: customRecipe.id,
|
||||
item: input.item.id,
|
||||
quantity: input.quantity,
|
||||
type: 'input'
|
||||
});
|
||||
}
|
||||
for (const req of recipe.requirements) {
|
||||
await db<CustomCraftingRecipeItem>('customCraftingRecipeItems')
|
||||
.insert({
|
||||
id: customRecipe.id,
|
||||
item: req.item.id,
|
||||
quantity: req.quantity,
|
||||
type: 'requirement'
|
||||
});
|
||||
}
|
||||
for (const output of recipe.outputs) {
|
||||
await db<CustomCraftingRecipeItem>('customCraftingRecipeItems')
|
||||
.insert({
|
||||
id: customRecipe.id,
|
||||
item: output.item.id,
|
||||
quantity: output.quantity,
|
||||
type: 'output'
|
||||
});
|
||||
}
|
||||
|
||||
buttonInteraction.reply({
|
||||
ephemeral: true,
|
||||
content: 'Your recipe has been created 🎉'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} satisfies Command;
|
|
@ -733,6 +733,7 @@ export default {
|
|||
});
|
||||
bot.on(Events.InteractionCreate, interaction => {
|
||||
if (!interaction.isModalSubmit()) return;
|
||||
if (!interaction.customId.startsWith('survey-')) return;
|
||||
if (!interaction.member) return;
|
||||
|
||||
const member = interaction.member as GuildMember;
|
||||
|
|
12
src/index.ts
12
src/index.ts
|
@ -1,12 +1,13 @@
|
|||
import { Client, GatewayIntentBits, Events, Collection, CommandInteraction, CommandInteractionOption, ApplicationCommandOptionType } from 'discord.js';
|
||||
import * as fs from 'fs';
|
||||
const { token } = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
const { token, sitePort, siteURL } = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
import * as path from 'path';
|
||||
import { initializeAnnouncements } from './lib/subscriptions';
|
||||
import * as log from './lib/log';
|
||||
import chalk from 'chalk';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { Command } from './types/index';
|
||||
import { startServer } from './web';
|
||||
|
||||
const bot = new Client({
|
||||
intents: [
|
||||
|
@ -20,9 +21,16 @@ const bot = new Client({
|
|||
],
|
||||
});
|
||||
|
||||
bot.config = {
|
||||
token, sitePort, siteURL
|
||||
};
|
||||
|
||||
async function init() {
|
||||
log.nonsense('booting chip...');
|
||||
|
||||
log.nonsense('starting up web interface...');
|
||||
await startServer(sitePort);
|
||||
|
||||
log.nonsense('setting up connection...');
|
||||
|
||||
try {
|
||||
|
@ -44,7 +52,7 @@ bot.on(Events.ClientReady, async () => {
|
|||
bot.commands = new Collection();
|
||||
const cmdFiles = fs.readdirSync(path.join(__dirname, './commands')).filter((file) => file.endsWith('.js'));
|
||||
for (const file of cmdFiles) {
|
||||
const cmd = (await import(`./commands/${file}`)) as Command;
|
||||
const cmd = (await import(`./commands/${file}`)).default as Command;
|
||||
bot.commands.set(cmd.data.name, cmd);
|
||||
if (cmd.onClientReady) cmd.onClientReady(bot);
|
||||
}
|
||||
|
|
|
@ -68,4 +68,14 @@ export interface CraftingStationCooldown {
|
|||
station: string,
|
||||
user: string,
|
||||
usedAt: number
|
||||
}
|
||||
export interface CustomCraftingRecipe {
|
||||
id: number,
|
||||
station: string
|
||||
}
|
||||
export interface CustomCraftingRecipeItem {
|
||||
id: number,
|
||||
item: number,
|
||||
quantity: number,
|
||||
type: 'input' | 'output' | 'requirement'
|
||||
}
|
|
@ -323,6 +323,19 @@ function changeCounterInteractionBuilder(linked: boolean) {
|
|||
return interaction.followUp(`You cannot **${amount > 0 ? (linked ? 'put' : 'produce') : (linked ? 'take' : 'consume')}** ${counter.emoji}.`);
|
||||
}
|
||||
|
||||
const min = await getCounterConfig(counter.id, 'min') as number;
|
||||
const max = await getCounterConfig(counter.id, 'max') as number;
|
||||
if (counter.value + amount < min) {
|
||||
if (min === 0) {
|
||||
return interaction.followUp(`You cannot remove more than how much is in the counter (${counter.value}x ${counter.emoji})!`);
|
||||
} else {
|
||||
return interaction.followUp(`You cannot decrement past the minimum value (${min})!`);
|
||||
}
|
||||
}
|
||||
if (counter.value + amount > max) {
|
||||
return interaction.followUp(`You are adding more than the counter can hold (${max}x)!`);
|
||||
}
|
||||
|
||||
let item;
|
||||
let newInv;
|
||||
if (linked) {
|
||||
|
@ -346,19 +359,6 @@ function changeCounterInteractionBuilder(linked: boolean) {
|
|||
newInv = await giveItem(member.id, item, amtInv);
|
||||
}
|
||||
|
||||
const min = await getCounterConfig(counter.id, 'min') as number;
|
||||
const max = await getCounterConfig(counter.id, 'max') as number;
|
||||
if (counter.value + amount < min) {
|
||||
if (min === 0) {
|
||||
return interaction.followUp(`You cannot remove more than how much is in the counter (${counter.value}x ${counter.emoji})!`);
|
||||
} else {
|
||||
return interaction.followUp(`You cannot decrement past the minimum value (${min})!`);
|
||||
}
|
||||
}
|
||||
if (counter.value + amount > max) {
|
||||
return interaction.followUp(`You are adding more than the counter can hold (${max}x)!`);
|
||||
}
|
||||
|
||||
const newCount = await changeCounter(counter.id, amount);
|
||||
await updateCounter(interaction.client, counter, newCount);
|
||||
await announceCounterUpdate(interaction.client, member, amount, counter, newCount, linked);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { pickRandom } from '../util';
|
||||
import { DefaultItems, Item, Items, formatItems, formatItemsArray, getDefaultItem, getItemQuantity } from './items';
|
||||
import { DefaultItems, Item, Items, formatItem, formatItems, formatItemsArray, getDefaultItem, getItemQuantity } from './items';
|
||||
|
||||
export interface CraftingStation {
|
||||
key: string,
|
||||
|
@ -65,7 +65,27 @@ export const craftingStations: CraftingStation[] = [
|
|||
description: 'A place for you to work with tools, for simple things',
|
||||
emoji: '🛠️',
|
||||
requires: getDefaultItem(DefaultItems.WORKBENCH)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fishing',
|
||||
name: 'Fishing',
|
||||
verb: 'Fished up',
|
||||
description: 'fish gaming wednesday',
|
||||
emoji: '🎣',
|
||||
requires: getDefaultItem(DefaultItems.FISHING_ROD),
|
||||
formatRecipe: (inputs, requirements, outputs, disableBold = false) =>
|
||||
`${formatItemsArray(inputs, disableBold)} => ${outputs.map(i => formatItem(i.item, disableBold) + '?').join(' ')}`,
|
||||
// weighted random
|
||||
manipulateResults: (outputs) => {
|
||||
const pool: Item[] = [];
|
||||
for (const out of outputs) {
|
||||
for (let i = 0; i < out.quantity; i++) {
|
||||
pool.push(out.item);
|
||||
}
|
||||
}
|
||||
return [{ item: pickRandom(pool), quantity: 1 }];
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export async function canUseStation(user: string, station: CraftingStation) {
|
||||
|
|
|
@ -19,6 +19,13 @@ export enum DefaultItems {
|
|||
LOG = 7,
|
||||
AXE = 8,
|
||||
BLOOD = 9,
|
||||
BAIT = 10,
|
||||
FISHING_ROD = 11,
|
||||
CARP = 12,
|
||||
PUFFERFISH = 13,
|
||||
EXOTIC_FISH = 14,
|
||||
SHOVEL = 15,
|
||||
DIRT = 16,
|
||||
}
|
||||
|
||||
export const defaultItems: DefaultItem[] = [
|
||||
|
@ -102,6 +109,69 @@ export const defaultItems: DefaultItem[] = [
|
|||
maxStack: 1024,
|
||||
untradable: false
|
||||
},
|
||||
{
|
||||
id: -10,
|
||||
name: 'Bait',
|
||||
description: 'I guess you could eat this.',
|
||||
emoji: '🪱',
|
||||
type: 'consumable',
|
||||
maxStack: 128,
|
||||
untradable: false
|
||||
},
|
||||
{
|
||||
id: -11,
|
||||
name: 'Fishing Rod',
|
||||
description: 'Give a man a fish, and he will eat for a day',
|
||||
emoji: '🎣',
|
||||
type: 'plain',
|
||||
maxStack: 1,
|
||||
untradable: false
|
||||
},
|
||||
{
|
||||
id: -12,
|
||||
name: 'Carp',
|
||||
description: 'wow',
|
||||
emoji: '🐟️',
|
||||
type: 'plain',
|
||||
maxStack: 16,
|
||||
untradable: false
|
||||
},
|
||||
{
|
||||
id: -13,
|
||||
name: 'Pufferfish',
|
||||
description: 'yummy!',
|
||||
emoji: '🐡',
|
||||
type: 'plain',
|
||||
maxStack: 16,
|
||||
untradable: false
|
||||
},
|
||||
{
|
||||
id: -14,
|
||||
name: 'Exotic Fish',
|
||||
description: 'lucky!',
|
||||
emoji: '🐠',
|
||||
type: 'plain',
|
||||
maxStack: 16,
|
||||
untradable: false,
|
||||
},
|
||||
{
|
||||
id: -15,
|
||||
name: 'Shovel',
|
||||
description: 'Did you know there\'s no shovel emoji',
|
||||
emoji: '♠️',
|
||||
type: 'plain',
|
||||
maxStack: 1,
|
||||
untradable: false,
|
||||
},
|
||||
{
|
||||
id: -16,
|
||||
name: 'Dirt',
|
||||
description: 'https://media.discordapp.net/attachments/819472665291128873/1081454188325785650/ezgif-2-5ccc7dedf8.gif',
|
||||
emoji: '🟫',
|
||||
type: 'consumable',
|
||||
maxStack: 64,
|
||||
untradable: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -91,7 +91,57 @@ export const defaultRecipes: DefaultRecipe[] = [
|
|||
outputs: [
|
||||
{ item: getDefaultItem(DefaultItems.BLOOD), quantity: 6 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: -8,
|
||||
station: 'fishing',
|
||||
inputs: [
|
||||
{ item: getDefaultItem(DefaultItems.BAIT), quantity: 1 }
|
||||
],
|
||||
requirements: [],
|
||||
outputs: [
|
||||
{ item: getDefaultItem(DefaultItems.CARP), quantity: 12 },
|
||||
{ item: getDefaultItem(DefaultItems.PUFFERFISH), quantity: 4 },
|
||||
{ item: getDefaultItem(DefaultItems.EXOTIC_FISH), quantity: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: -9,
|
||||
station: 'workbench',
|
||||
inputs: [
|
||||
{ item: getDefaultItem(DefaultItems.TWIG), quantity: 2 },
|
||||
{ item: getDefaultItem(DefaultItems.BAIT), quantity: 1 },
|
||||
],
|
||||
requirements: [],
|
||||
outputs: [
|
||||
{ item: getDefaultItem(DefaultItems.FISHING_ROD), quantity: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: -10,
|
||||
station: 'forage',
|
||||
inputs: [],
|
||||
requirements: [
|
||||
{ item: getDefaultItem(DefaultItems.SHOVEL), quantity: 1 },
|
||||
],
|
||||
outputs: [
|
||||
{ item: getDefaultItem(DefaultItems.BAIT), quantity: 3 },
|
||||
{ item: getDefaultItem(DefaultItems.PEBBLE), quantity: 1 },
|
||||
{ item: getDefaultItem(DefaultItems.DIRT), quantity: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: -11,
|
||||
station: 'workbench',
|
||||
inputs: [
|
||||
{ item: getDefaultItem(DefaultItems.PEBBLE), quantity: 3 },
|
||||
{ item: getDefaultItem(DefaultItems.TWIG), quantity: 2 },
|
||||
],
|
||||
requirements: [],
|
||||
outputs: [
|
||||
{ item: getDefaultItem(DefaultItems.SHOVEL), quantity: 1 },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
export function getDefaultRecipe(id: number): DefaultRecipe | undefined {
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import { Collection, SlashCommandBuilder, CommandInteraction, Client } from 'discord.js';
|
||||
|
||||
export interface Command {
|
||||
data: Pick<SlashCommandBuilder, "toJSON" | "name">,
|
||||
execute: (interaction: CommandInteraction) => Promise<any>,
|
||||
autocomplete?: (interaction: AutocompleteInteraction) => Promise<any>,
|
||||
onClientReady?: (client: Client) => Promise<any>,
|
||||
data: Pick<SlashCommandBuilder, 'toJSON' | 'name'>,
|
||||
execute: (interaction: CommandInteraction) => Promise<unknown>,
|
||||
autocomplete?: (interaction: AutocompleteInteraction) => Promise<unknown>,
|
||||
onClientReady?: (client: Client) => Promise<unknown>,
|
||||
serverWhitelist?: string[],
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
token: string,
|
||||
sitePort: number,
|
||||
siteURL: string
|
||||
}
|
||||
|
||||
declare module 'discord.js' {
|
||||
export interface Client {
|
||||
config: Config,
|
||||
commands: Collection<string, Command>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import express from 'express';
|
||||
import * as log from './lib/log';
|
||||
import { CustomItem, db } from './lib/db';
|
||||
import { defaultItems } from './lib/rpg/items';
|
||||
|
||||
export async function startServer(port: number) {
|
||||
const app = express();
|
||||
|
||||
app.use(express.static('static/'));
|
||||
|
||||
app.get('/api/items', async (req, res) => {
|
||||
const guildID = req.query.guild;
|
||||
|
||||
let customItems : Partial<CustomItem>[];
|
||||
if (guildID) {
|
||||
customItems = await db<CustomItem>('customItems')
|
||||
.select('emoji', 'name', 'id', 'description')
|
||||
.where('guild', guildID)
|
||||
.limit(25);
|
||||
} else {
|
||||
customItems = [];
|
||||
}
|
||||
|
||||
res.json([...defaultItems, ...customItems]);
|
||||
});
|
||||
|
||||
app.listen(port, () => log.info(`web interface listening on ${port}`));
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,58 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>jillo</title>
|
||||
|
||||
<meta name="theme-color" content="light dark">
|
||||
<link href="/style.css" rel="stylesheet">
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Balsamiq+Sans&display=swap" rel="stylesheet">
|
||||
|
||||
<script src="/create-recipe/script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="header">
|
||||
<div class="bg"></div>
|
||||
<div class="left">
|
||||
<a href="/">jillo</a>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="https://discord.com/oauth2/authorize?client_id=898850107892596776&scope=bot" target="_blank" rel="noopener">invite</a>
|
||||
·
|
||||
<a href="https://git.oat.zone/dark-firepit/jillo-bot" target="_blank" rel="noopener">repo</a>
|
||||
<img src="/assets/jillo_small.png">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Recipe Creator</h1>
|
||||
|
||||
<div class="items">
|
||||
loading available items...
|
||||
</div>
|
||||
|
||||
<p class="note">Drag items from above into the below lists:</p>
|
||||
|
||||
<h2>Inputs <span class="subtitle">Ingredients necessary to create the outputs</span></h2>
|
||||
<div class="item-list" data-type="inputs">
|
||||
</div>
|
||||
<h2>Requirements <span class="subtitle">Unlike inputs, these are not consumed, but are necessary</span></h2>
|
||||
<div class="item-list" data-type="requirements">
|
||||
</div>
|
||||
<h2>Outputs <span class="subtitle">The result of the recipe</span></h2>
|
||||
<div class="item-list" data-type="outputs">
|
||||
</div>
|
||||
|
||||
<p class="note">Drag an item out of the list in order to remove it</p>
|
||||
|
||||
<h1>Recipe string</h1>
|
||||
<pre id="recipe-string"></pre>
|
||||
Copy-paste this into Jillo to create your recipe
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,145 @@
|
|||
let resolveLoaded;
|
||||
const loaded = new Promise(resolve => resolveLoaded = resolve);
|
||||
|
||||
function e(unsafeText) {
|
||||
let div = document.createElement('div');
|
||||
div.innerText = unsafeText;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('../../src/lib/rpg/items').Item | null}
|
||||
*/
|
||||
let draggedItem = null;
|
||||
/**
|
||||
* @type {Record<string, import('../../src/lib/rpg/items').Items[]>}
|
||||
*/
|
||||
let itemLists = {};
|
||||
|
||||
function listToString(list) {
|
||||
return list.map(stack => `${stack.item.id},${stack.quantity}`).join(';');
|
||||
}
|
||||
function updateString() {
|
||||
document.querySelector('#recipe-string').innerText = [
|
||||
listToString(itemLists['inputs'] || []),
|
||||
listToString(itemLists['requirements'] || []),
|
||||
listToString(itemLists['outputs'] || []),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../../src/lib/rpg/items').Item} item
|
||||
*/
|
||||
function renderItem(item) {
|
||||
const i = document.createElement('div');
|
||||
i.innerHTML = `
|
||||
<div class="icon">
|
||||
${e(item.emoji)}
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="name">${e(item.name)}</div>
|
||||
<div class="description">${item.description ? e(item.description) : '<i>No description</i>'}</div>
|
||||
</div>
|
||||
`;
|
||||
i.classList.add('item');
|
||||
i.draggable = true;
|
||||
i.addEventListener('dragstart', event => {
|
||||
draggedItem = item;
|
||||
event.target.classList.add('dragging');
|
||||
});
|
||||
i.addEventListener('dragend', event => {
|
||||
draggedItem = null;
|
||||
event.target.classList.remove('dragging');
|
||||
});
|
||||
return i;
|
||||
}
|
||||
function renderItemStack(item, quantity, type) {
|
||||
const i = document.createElement('div');
|
||||
i.innerHTML = `
|
||||
<div class="icon">
|
||||
${e(item.emoji)}
|
||||
</div>
|
||||
<div class="right">
|
||||
x<b>${quantity}</b>
|
||||
</div>
|
||||
`;
|
||||
i.classList.add('itemstack');
|
||||
i.draggable = true;
|
||||
i.addEventListener('dragstart', event => {
|
||||
event.target.classList.add('dragging');
|
||||
});
|
||||
i.addEventListener('dragend', event => {
|
||||
event.target.classList.remove('dragging');
|
||||
itemLists[type] = itemLists[type] || [];
|
||||
const items = itemLists[type];
|
||||
const stackIdx = items.findIndex(n => n.item.id === item.id);
|
||||
if (stackIdx !== -1) items.splice(stackIdx, 1);
|
||||
document.querySelector(`.item-list[data-type="${type}"]`).replaceWith(renderItemList(items, type));
|
||||
updateString();
|
||||
});
|
||||
return i;
|
||||
}
|
||||
function renderItemList(items, type) {
|
||||
const i = document.createElement('div');
|
||||
i.textContent = '';
|
||||
items.forEach(itemStack => {
|
||||
i.appendChild(renderItemStack(itemStack.item, itemStack.quantity, type));
|
||||
});
|
||||
i.dataset.type = type;
|
||||
i.classList.add('item-list');
|
||||
|
||||
// prevent default to allow drop
|
||||
i.addEventListener('dragover', (event) => event.preventDefault(), false);
|
||||
|
||||
i.addEventListener('dragenter', event => draggedItem && event.target.classList.add('dropping'));
|
||||
i.addEventListener('dragleave', event => draggedItem && event.target.classList.remove('dropping'));
|
||||
|
||||
i.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
event.target.classList.remove('dropping');
|
||||
|
||||
if (!draggedItem) return;
|
||||
|
||||
itemLists[type] = itemLists[type] || [];
|
||||
const items = itemLists[type];
|
||||
|
||||
const itemStack = items.find(v => v.item.id === draggedItem.id);
|
||||
|
||||
if (!itemStack) {
|
||||
items.push({
|
||||
item: draggedItem,
|
||||
quantity: 1
|
||||
});
|
||||
} else {
|
||||
itemStack.quantity = itemStack.quantity + 1;
|
||||
}
|
||||
|
||||
updateString();
|
||||
|
||||
draggedItem = null;
|
||||
|
||||
event.target.replaceWith(renderItemList(items, type));
|
||||
});
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
fetch(`/api/items${document.location.search}`)
|
||||
.then(res => res.json()),
|
||||
loaded
|
||||
]).then(items => {
|
||||
const itemsContainer = document.querySelector('.items');
|
||||
itemsContainer.textContent = '';
|
||||
items[0].forEach(item =>
|
||||
itemsContainer.appendChild(renderItem(item))
|
||||
);
|
||||
document.querySelectorAll('.item-list').forEach(elem => {
|
||||
const type = elem.dataset.type;
|
||||
elem.replaceWith(renderItemList([], type));
|
||||
});
|
||||
|
||||
updateString();
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => resolveLoaded());
|
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1,27 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>jillo</title>
|
||||
|
||||
<meta name="theme-color" content="light dark">
|
||||
<link href="/style.css" rel="stylesheet">
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Balsamiq+Sans&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<img src="/assets/jillo.png" width="150" height="200">
|
||||
<h1>jillo!</h1>
|
||||
<div>
|
||||
<a href="https://discord.com/oauth2/authorize?client_id=898850107892596776&scope=bot" target="_blank" rel="noopener">invite</a>
|
||||
·
|
||||
<a href="https://git.oat.zone/dark-firepit/jillo-bot" target="_blank" rel="noopener">repo</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,250 @@
|
|||
:root {
|
||||
--accent-color: #f17d10;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
overflow-x: hidden;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
font-family: 'Balsamiq Sans', sans-serif;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
text-underline-offset: 3px;
|
||||
font-size: 16px;
|
||||
color-scheme: light dark;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
--text-color: #111;
|
||||
--text-color-light: #444;
|
||||
--background-color: #fefefd;
|
||||
--background-color-dark: #fafafb;
|
||||
--background-color-dark-2: #f8f8f9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-color: #eee;
|
||||
--text-color-light: #aaa;
|
||||
--background-color: #111110;
|
||||
--background-color-dark: #151514;
|
||||
--background-color-dark-2: #171718;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#main {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
#main img {
|
||||
display: block;
|
||||
height: 18rem;
|
||||
width: auto;
|
||||
animation: 1s popup;
|
||||
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition: transform 0.15s, opacity 0.1s;
|
||||
}
|
||||
#main img:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
#main :not(img) {
|
||||
animation: 0.8s fadein;
|
||||
}
|
||||
#main h1 {
|
||||
font-size: 4rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@keyframes popup {
|
||||
0% { transform: scale(0) rotate(40deg) }
|
||||
100% { transform: scale(1) rotate(0deg) }
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0rem 1rem;
|
||||
flex-direction: row;
|
||||
text-shadow: 2px 2px 2px #000000;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.header .bg {
|
||||
position: absolute;
|
||||
left: 0%;
|
||||
right: 0%;
|
||||
height: 3rem;
|
||||
background: linear-gradient(#444, #222);
|
||||
z-index: -1;
|
||||
border-bottom: 2px ridge #aaa;
|
||||
}
|
||||
.header .left {
|
||||
font-size: 1.5rem;
|
||||
flex: 1 1 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header .left a {
|
||||
text-decoration: none !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header .links {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.header .links img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.items {
|
||||
max-width: 500px;
|
||||
padding: 1em;
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
background: linear-gradient(var(--background-color-dark), var(--background-color-dark-2));
|
||||
border-radius: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 3rem;
|
||||
gap: 0.5rem;
|
||||
outline: 0px solid rgba(255, 255, 255, 0.0);
|
||||
transition: outline 0.1s;
|
||||
padding: 0.5rem;
|
||||
border-radius: 2rem;
|
||||
cursor: grab;
|
||||
}
|
||||
.item:hover {
|
||||
outline: 1px solid var(--text-color-light);
|
||||
}
|
||||
.item.dragging {
|
||||
outline: 2px dashed var(--text-color-light);
|
||||
transition: none;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
.item .icon {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
background-color: rgba(199, 199, 199, 0.25);
|
||||
border-radius: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1 / 1;
|
||||
user-select: none;
|
||||
}
|
||||
.item .right {
|
||||
flex: 1 1 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
}
|
||||
.item .right, .item .right > * {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.item .right .description {
|
||||
font-size: 75%;
|
||||
color: var(--text-color-light);
|
||||
}
|
||||
.item-list {
|
||||
min-height: 2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.item-list.dropping::after {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
content: '+';
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
outline: 2px dashed var(--text-color-light);
|
||||
border-radius: 2rem;
|
||||
padding: 0.5rem;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
.itemstack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 1.5rem;
|
||||
line-height: 1;
|
||||
padding: 0.5rem;
|
||||
margin: 0.25rem;
|
||||
outline: 1px solid var(--text-color-light);
|
||||
background-color: var(--background-color-dark);
|
||||
border-radius: 2rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.itemstack.dragging {
|
||||
outline: 2px dashed var(--text-color-light);
|
||||
transition: none;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-color-light);
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.subtitle::before {
|
||||
content: '·';
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: linear-gradient(var(--background-color-dark), var(--background-color-dark-2));
|
||||
overflow: auto;
|
||||
word-break: normal;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-style: italic;
|
||||
color: var(--text-color-light);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
});
|
Loading…
Reference in New Issue