Compare commits
5 Commits
783af8652c
...
ea24a931ca
Author | SHA1 | Date |
---|---|---|
Jill | ea24a931ca | |
Jill | 8ad18e9b19 | |
Jill | c4980da8b7 | |
Jill | f44f79a955 | |
Jill | b862028524 |
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.createTable('invincibleUsers', table => {
|
||||
table.string('user').notNullable().unique();
|
||||
table.timestamp('since').notNullable();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.dropTable('invincibleUsers');
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.createTable('itemBehaviors', table => {
|
||||
table.integer('item').notNullable();
|
||||
table.string('behavior').notNullable();
|
||||
table.float('value');
|
||||
})
|
||||
.alterTable('customItems', table => {
|
||||
table.dropColumn('behavior');
|
||||
table.dropColumn('behaviorValue');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
exports.down = function(knex) {
|
||||
// no
|
||||
throw 'Not implemented';
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { weaponAutocomplete, getItem, getItemQuantity, formatItems } from '../lib/rpg/items';
|
||||
import { weaponAutocomplete, getItem, getItemQuantity, formatItems, formatItem } from '../lib/rpg/items';
|
||||
import { Command } from '../types/index';
|
||||
import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID } from '../lib/rpg/pvp';
|
||||
import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID, resetInvincible, INVINCIBLE_TIMER, getInvincibleMs } from '../lib/rpg/pvp';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
|
@ -35,12 +35,15 @@ export default {
|
|||
const weapon = await getItem(weaponID);
|
||||
if (!weapon) return interaction.followUp('No such item exists!');
|
||||
if (weapon.type !== 'weapon') return interaction.followUp('That is not a weapon!');
|
||||
const invinTimer = await getInvincibleMs(user.id);
|
||||
if (invinTimer > 0) return interaction.followUp(`You can only attack this user <t:${Math.floor((Date.now() + invinTimer) / 1000)}:R> (or if they perform an action first)!`);
|
||||
|
||||
const dmg = weapon.maxStack;
|
||||
await dealDamage(user.id, dmg);
|
||||
const newHealth = await getItemQuantity(user.id, BLOOD_ID);
|
||||
|
||||
await interaction.followUp(`You hit ${user} for ${BLOOD_ITEM.emoji} **${dmg}** damage! They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.`);
|
||||
if (user.id !== member.id) await resetInvincible(member.id);
|
||||
await interaction.followUp(`You hit ${user} with ${formatItem(weapon)} for ${BLOOD_ITEM.emoji} **${dmg}** damage! They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.\nYou can attack them again <t:${Math.floor((Date.now() + INVINCIBLE_TIMER) / 1000)}:R> (or if they perform an action first).`);
|
||||
},
|
||||
|
||||
autocomplete: weaponAutocomplete,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getStation, canUseStation, craftingStations, verb, CraftingStation } fr
|
|||
import { formatItem, getItemQuantity, formatItems, getMaxStack, giveItem, formatItemsArray } from '../lib/rpg/items';
|
||||
import { getRecipe, defaultRecipes, formatRecipe, resolveCustomRecipe } from '../lib/rpg/recipes';
|
||||
import { Command } from '../types/index';
|
||||
import { initHealth } from '../lib/rpg/pvp';
|
||||
import { initHealth, resetInvincible } from '../lib/rpg/pvp';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
|
@ -98,6 +98,7 @@ export default {
|
|||
nextUsableAt = Date.now() + station.cooldown * 1000;
|
||||
}
|
||||
|
||||
await resetInvincible(member.id);
|
||||
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>` : ''}`);
|
||||
},
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { AutocompleteInteraction, CommandInteraction, SlashCommandBuilder } from 'discord.js';
|
||||
import { CustomCraftingRecipeItem, CustomItem, db } from '../lib/db';
|
||||
import { Counter, CustomCraftingRecipeItem, CustomItem, db } from '../lib/db';
|
||||
import { customItemAutocomplete, formatItem, formatItems, getItem, giveItem, itemAutocomplete } from '../lib/rpg/items';
|
||||
import { behaviors } from '../lib/rpg/behaviors';
|
||||
import { Command } from '../types/index';
|
||||
import { formatRecipe, getCustomRecipe } from '../lib/rpg/recipes';
|
||||
|
||||
|
@ -43,22 +42,11 @@ export default {
|
|||
.setName('maxstack')
|
||||
.setDescription('Maximum amount of this item you\'re able to hold at once')
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('behavior')
|
||||
.setDescription('Special behavior type')
|
||||
.setChoices(...behaviors.filter(b => b.itemType === 'plain').map(b => ({name: `${b.name} - ${b.description}`, value: b.name})))
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('untradable')
|
||||
.setDescription('Can you give this item to other people?')
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt
|
||||
.setName('behaviorvalue')
|
||||
.setDescription('A value to use for the behavior type; not always applicable')
|
||||
)
|
||||
)
|
||||
.addSubcommand(cmd =>
|
||||
cmd
|
||||
|
@ -87,22 +75,11 @@ export default {
|
|||
.setName('description')
|
||||
.setDescription('A short description')
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('behavior')
|
||||
.setDescription('Special behavior type')
|
||||
.setChoices(...behaviors.filter(b => b.itemType === 'weapon').map(b => ({name: `${b.name} - ${b.description}`, value: b.name})))
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('untradable')
|
||||
.setDescription('Can you give this item to other people?')
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt
|
||||
.setName('behaviorvalue')
|
||||
.setDescription('A value to use for the behavior type; not always applicable')
|
||||
)
|
||||
)
|
||||
.addSubcommand(cmd =>
|
||||
cmd
|
||||
|
@ -130,22 +107,11 @@ export default {
|
|||
.setName('maxstack')
|
||||
.setDescription('Maximum amount of this item you\'re able to hold at once')
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('behavior')
|
||||
.setDescription('Special behavior type')
|
||||
.setChoices(...behaviors.filter(b => b.itemType === 'consumable').map(b => ({name: `${b.name} - ${b.description}`, value: b.name})))
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('untradable')
|
||||
.setDescription('Can you give this item to other people?')
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt
|
||||
.setName('behaviorvalue')
|
||||
.setDescription('A value to use for the behavior type; not always applicable')
|
||||
)
|
||||
)
|
||||
)
|
||||
.addSubcommand(cmd =>
|
||||
|
@ -203,9 +169,7 @@ export default {
|
|||
'emoji': interaction.options.getString('emoji', true).trim(),
|
||||
'type': subcommand as 'plain' | 'weapon' | 'consumable', // kind of wild that ts makes you do this
|
||||
'maxStack': (interaction.options.getInteger('maxstack') || interaction.options.getInteger('damage')) || (subcommand === 'weapon' ? 1 : 64),
|
||||
'behavior': interaction.options.getString('behavior') || undefined,
|
||||
'untradable': interaction.options.getBoolean('untradable') || false,
|
||||
'behaviorValue': interaction.options.getNumber('behaviorValue') || undefined,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
|
@ -236,6 +200,13 @@ export default {
|
|||
return interaction.followUp(`⚠️ This item is used in the following recipes:\n${recipes.map(r => `- ${formatRecipe(r!)}`).join('\n')}`);
|
||||
}
|
||||
|
||||
const linkedWith = await db<Counter>('counters')
|
||||
.where('linkedItem', item.id);
|
||||
|
||||
if (linkedWith.length > 0) {
|
||||
return interaction.followUp(`⚠️ This item is used in the following counters:\n${linkedWith.map(c => `- ${c.key} ${c.value} in <#${c.channel}>`).join('\n')}`);
|
||||
}
|
||||
|
||||
await db<CustomItem>('customItems')
|
||||
.where('id', item.id)
|
||||
.delete();
|
||||
|
|
|
@ -8,6 +8,7 @@ import chalk from 'chalk';
|
|||
import prettyBytes from 'pretty-bytes';
|
||||
import { Command } from './types/index';
|
||||
import { startServer } from './web/web';
|
||||
import { init as initPVP } from './lib/rpg/pvp';
|
||||
|
||||
const bot = new Client({
|
||||
intents: [
|
||||
|
@ -40,6 +41,8 @@ async function init() {
|
|||
log.error(`${chalk.bold('emergency mode could not be established.')} shutting down.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
initPVP(bot);
|
||||
}
|
||||
|
||||
bot.on(Events.ClientReady, async () => {
|
||||
|
|
|
@ -55,9 +55,7 @@ export interface CustomItem {
|
|||
type: 'plain' | 'weapon' | 'consumable',
|
||||
// also damage for weapons; weapons are always unstackable (cus i said so)
|
||||
maxStack: number,
|
||||
behavior?: string,
|
||||
untradable: boolean,
|
||||
behaviorValue?: number
|
||||
}
|
||||
export interface ItemInventory {
|
||||
user: string,
|
||||
|
@ -89,4 +87,13 @@ export interface Session {
|
|||
}
|
||||
export interface InitHealth {
|
||||
user: string,
|
||||
}
|
||||
export interface InvincibleUser {
|
||||
user: string,
|
||||
since: number,
|
||||
}
|
||||
export interface ItemBehavior {
|
||||
item: number,
|
||||
behavior: string,
|
||||
value?: number
|
||||
}
|
|
@ -1,26 +1,78 @@
|
|||
import type { User } from 'discord.js';
|
||||
import type { Item } from './items';
|
||||
import { giveItem, type Item, isDefaultItem } from './items';
|
||||
import { Either, Left } from '../util';
|
||||
import { ItemBehavior, db } from '../db';
|
||||
import { BLOOD_ITEM, dealDamage } from './pvp';
|
||||
|
||||
export interface Behavior {
|
||||
name: string,
|
||||
description: string,
|
||||
itemType: 'plain' | 'weapon' | 'consumable',
|
||||
// make it look fancy
|
||||
format?: (value: number) => string,
|
||||
// triggers upon use
|
||||
// for 'weapons', this is on hit
|
||||
// for 'consumable', this is on use
|
||||
// for 'plain', ...??
|
||||
// returns `true` upon success, `false` otherwise
|
||||
action?: (item: Item, user: User) => Promise<boolean>
|
||||
// for 'weapons', this is on attack
|
||||
// for 'consumable' and `plain`, this is on use
|
||||
// returns Left upon success, the reason for failure otherwise (Right)
|
||||
onUse?: (value: number, item: Item, user: string) => Promise<Either<null, string>>,
|
||||
// triggers upon `weapons` attack
|
||||
// returns the new damage value upon success (Left), the reason for failure otherwise (Right)
|
||||
onAttack?: (value: number, item: Item, user: string, target: string, damage: number) => Promise<Either<number, string>>,
|
||||
}
|
||||
|
||||
export const behaviors: Behavior[] = [
|
||||
{
|
||||
name: 'heal',
|
||||
description: 'Heals the user by `behaviorValue`',
|
||||
description: 'Heals the user by `value`',
|
||||
itemType: 'consumable',
|
||||
action: async (item, user) => {
|
||||
// todo
|
||||
return false;
|
||||
}
|
||||
async onUse(value, item, user) {
|
||||
await dealDamage(user, -Math.floor(value));
|
||||
return new Left(null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'damage',
|
||||
description: 'Damages the user by `value',
|
||||
itemType: 'consumable',
|
||||
async onUse(value, item, user) {
|
||||
await dealDamage(user, Math.floor(value));
|
||||
return new Left(null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'random_up',
|
||||
description: 'Randomizes the attack value up by a maximum of `value`',
|
||||
itemType: 'weapon',
|
||||
format: (value) => `random +${value}`,
|
||||
async onAttack(value, item, user, target, damage) {
|
||||
return new Left(damage + Math.round(Math.random() * value));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'random_down',
|
||||
description: 'Randomizes the attack value down by a maximum of `value`',
|
||||
itemType: 'weapon',
|
||||
format: (value) => `random -${value}`,
|
||||
async onAttack(value, item, user, target, damage) {
|
||||
return new Left(damage - Math.round(Math.random() * value));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lifesteal',
|
||||
description: 'Gain blood by stabbing your foes, scaled by `value`x',
|
||||
itemType: 'weapon',
|
||||
format: (value) => `lifesteal: ${value}x`,
|
||||
async onAttack(value, item, user, target, damage) {
|
||||
giveItem(user, BLOOD_ITEM, Math.floor(damage * value));
|
||||
return new Left(damage);
|
||||
},
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
export async function getBehaviors(item: Item) {
|
||||
if (isDefaultItem(item)) {
|
||||
return item.behaviors || [];
|
||||
} else {
|
||||
return await db<ItemBehavior>('itemBehaviors')
|
||||
.where('item', item.id);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, Aut
|
|||
import { getSign } from '../util';
|
||||
import { Counter, CounterConfiguration, CounterUserLink, db } from '../db';
|
||||
import { formatItems, getItem, getItemQuantity, getMaxStack, giveItem } from './items';
|
||||
import { resetInvincible } from './pvp';
|
||||
|
||||
export async function getCounter(id: number) {
|
||||
const counter = await db<Counter>('counters')
|
||||
|
@ -359,6 +360,7 @@ function changeCounterInteractionBuilder(linked: boolean) {
|
|||
newInv = await giveItem(member.id, item, amtInv);
|
||||
}
|
||||
|
||||
await resetInvincible(member.id);
|
||||
const newCount = await changeCounter(counter.id, amount);
|
||||
await updateCounter(interaction.client, counter, newCount);
|
||||
await announceCounterUpdate(interaction.client, member, amount, counter, newCount, linked);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { AutocompleteInteraction } from 'discord.js';
|
||||
import { CustomItem, ItemInventory, db } from '../db';
|
||||
import { CustomItem, ItemBehavior, ItemInventory, db } from '../db';
|
||||
|
||||
export type DefaultItem = Omit<CustomItem, 'guild'>; // uses negative IDs
|
||||
// uses negative IDs
|
||||
export type DefaultItem = Omit<CustomItem, 'guild'> & { behaviors?: Omit<ItemBehavior, 'item'>[] };
|
||||
export type Item = DefaultItem | CustomItem;
|
||||
|
||||
export interface Items {
|
||||
|
@ -324,7 +325,7 @@ export async function giveItem(user: string, item: Item, quantity = 1): Promise<
|
|||
|
||||
let inv;
|
||||
if (storedItem) {
|
||||
if (storedItem.quantity + quantity === 0) {
|
||||
if (storedItem.quantity + quantity === 0 && item.id !== DefaultItems.BLOOD) { // let blood show as 0x
|
||||
await db<ItemInventory>('itemInventories')
|
||||
.delete()
|
||||
.where('user', user)
|
||||
|
@ -361,6 +362,10 @@ export function getMaxStack(item: Item) {
|
|||
return item.type === 'weapon' ? 1 : item.maxStack;
|
||||
}
|
||||
|
||||
export function isDefaultItem(item: Item): item is DefaultItem {
|
||||
return (item as DefaultItem).behaviors !== undefined; // ough
|
||||
}
|
||||
|
||||
export function formatItem(item: Item | undefined, disableBold = false) {
|
||||
if (!item) return disableBold ? '? MISSINGNO' : '? **MISSINGNO**';
|
||||
return disableBold ? `${item.emoji} ${item.name}` : `${item.emoji} **${item.name}**`;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { InitHealth, ItemInventory, db } from '../db';
|
||||
import { InitHealth, InvincibleUser, ItemInventory, db } from '../db';
|
||||
import { DefaultItems, getDefaultItem, giveItem, getItemQuantity, formatItems } from './items';
|
||||
import { Client } from 'discord.js';
|
||||
|
||||
export const BLOOD_ID = DefaultItems.BLOOD;
|
||||
export const BLOOD_ID = -DefaultItems.BLOOD;
|
||||
export const BLOOD_ITEM = getDefaultItem(BLOOD_ID);
|
||||
export const MAX_HEALTH = BLOOD_ITEM.maxStack;
|
||||
const BLOOD_GAIN_PER_HOUR = 5;
|
||||
export const INVINCIBLE_TIMER = 1_000 * 60 * 60;
|
||||
|
||||
export async function initHealth(user: string) {
|
||||
const isInitialized = await db<InitHealth>('initHealth')
|
||||
|
@ -18,11 +19,43 @@ export async function initHealth(user: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getInvincibleMs(user: string) {
|
||||
const invincible = await db<InvincibleUser>('invincibleUsers')
|
||||
.where('user', user)
|
||||
.first();
|
||||
|
||||
if (!invincible) return 0;
|
||||
return Math.max((invincible.since + INVINCIBLE_TIMER) - Date.now(), 0);
|
||||
}
|
||||
|
||||
export async function resetInvincible(user: string) {
|
||||
await db<InvincibleUser>('invincibleUsers')
|
||||
.where('user', user)
|
||||
.delete();
|
||||
}
|
||||
|
||||
export async function applyInvincible(user: string) {
|
||||
const exists = await db<InvincibleUser>('invincibleUsers')
|
||||
.where('user', user)
|
||||
.first();
|
||||
|
||||
if (exists) {
|
||||
await db<InvincibleUser>('invincibleUsers')
|
||||
.update({ since: Date.now() });
|
||||
} else {
|
||||
await db<InvincibleUser>('invincibleUsers')
|
||||
.insert({ since: Date.now(), user });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHealth(user: string) {
|
||||
await initHealth(user);
|
||||
return await getItemQuantity(user, BLOOD_ID);
|
||||
}
|
||||
|
||||
export async function dealDamage(user: string, dmg: number) {
|
||||
await initHealth(user);
|
||||
await applyInvincible(user);
|
||||
return await giveItem(user, BLOOD_ITEM, -dmg);
|
||||
}
|
||||
|
||||
|
@ -45,5 +78,6 @@ async function healthCron(bot: Client) {
|
|||
}
|
||||
|
||||
export function init(bot: Client) {
|
||||
healthCron(bot);
|
||||
setInterval(() => healthCron(bot), 1_000 * 60 * 60);
|
||||
}
|
|
@ -27,4 +27,17 @@ export async function writeTmpFile(data: string | Buffer, filename?: string, ext
|
|||
|
||||
export function pickRandom<T>(list: T[]): T {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
}
|
||||
}
|
||||
|
||||
// WE OUT HERE
|
||||
export type Either<L,R> = Left<L> | Right<R>
|
||||
|
||||
export class Left<L> {
|
||||
constructor(private readonly value: L) {}
|
||||
public getValue() { return this.value; }
|
||||
}
|
||||
|
||||
export class Right<R> {
|
||||
constructor(private readonly value: R) {}
|
||||
public getValue() { return this.value; }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue