behavior tweaks, rebalancing
This commit is contained in:
parent
1dee5ed060
commit
465c42437c
|
@ -2,6 +2,8 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js
|
|||
import { getItem, getItemQuantity, formatItems, formatItem, weaponInventoryAutocomplete } from '../lib/rpg/items';
|
||||
import { Command } from '../types/index';
|
||||
import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID, resetInvincible, INVINCIBLE_TIMER, getInvincibleMs } from '../lib/rpg/pvp';
|
||||
import { getBehavior, getBehaviors } from '../lib/rpg/behaviors';
|
||||
import { Right } from '../lib/util';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
|
@ -28,7 +30,7 @@ export default {
|
|||
const member = interaction.member! as GuildMember;
|
||||
await initHealth(member.id);
|
||||
const weaponID = parseInt(interaction.options.getString('weapon', true));
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const target = interaction.options.getUser('user', true);
|
||||
|
||||
await interaction.deferReply({ephemeral: true});
|
||||
|
||||
|
@ -37,15 +39,40 @@ export default {
|
|||
if (weapon.type !== 'weapon') return interaction.followUp('That is not a weapon!');
|
||||
const inv = await getItemQuantity(member.id, weapon.id);
|
||||
if (inv.quantity === 0) return interaction.followUp('You do not have this weapon!');
|
||||
const invinTimer = await getInvincibleMs(user.id);
|
||||
const invinTimer = await getInvincibleMs(target.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);
|
||||
let dmg = weapon.maxStack;
|
||||
const messages = [];
|
||||
|
||||
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).`);
|
||||
const behaviors = await getBehaviors(weapon);
|
||||
for (const itemBehavior of behaviors) {
|
||||
const behavior = getBehavior(itemBehavior.behavior);
|
||||
if (!behavior) continue;
|
||||
if (!behavior.onAttack) continue;
|
||||
const res = await behavior.onAttack({
|
||||
value: itemBehavior.value,
|
||||
damage: dmg,
|
||||
item: weapon,
|
||||
user: member.id,
|
||||
target: target.id,
|
||||
});
|
||||
if (res instanceof Right) {
|
||||
await interaction.followUp(`You tried to attack with ${formatItem(weapon)}... but failed!\n${res.getValue()}`);
|
||||
return;
|
||||
} else {
|
||||
const { message, damage } = res.getValue();
|
||||
if (message) messages.push(message);
|
||||
if (damage) dmg = damage;
|
||||
}
|
||||
}
|
||||
|
||||
await dealDamage(target.id, dmg);
|
||||
const newHealth = await getItemQuantity(target.id, BLOOD_ID);
|
||||
|
||||
if (target.id !== member.id) await resetInvincible(member.id);
|
||||
|
||||
await interaction.followUp(`You hit ${target} with ${formatItem(weapon)} for ${BLOOD_ITEM.emoji} **${dmg}** damage!\n${messages.map(m => `_${m}_\n`).join('')}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: weaponInventoryAutocomplete,
|
||||
|
|
|
@ -41,7 +41,11 @@ export default {
|
|||
const behavior = getBehavior(itemBehavior.behavior);
|
||||
if (!behavior) continue;
|
||||
if (!behavior.onUse) continue;
|
||||
const res = await behavior.onUse(itemBehavior.value, item, member.id);
|
||||
const res = await behavior.onUse({
|
||||
value: itemBehavior.value,
|
||||
item,
|
||||
user: member.id
|
||||
});
|
||||
if (res instanceof Right) {
|
||||
await interaction.followUp(`You tried to eat ${formatItems(item, 1)}... but failed!\n${res.getValue()}`);
|
||||
return;
|
||||
|
|
|
@ -332,11 +332,11 @@ export default {
|
|||
if (itemID) {
|
||||
const item = await getItem(parseInt(itemID));
|
||||
if (item) {
|
||||
foundBehaviors = foundBehaviors.filter(b => b.itemType === item.type);
|
||||
foundBehaviors = foundBehaviors.filter(b => b.type === item.type);
|
||||
}
|
||||
}
|
||||
|
||||
return foundBehaviors.map(b => ({name: `${b.itemType}:${b.name} - ${b.description}`, value: b.name}));
|
||||
return foundBehaviors.map(b => ({name: `${b.type}:${b.name} - ${b.description}`, value: b.name}));
|
||||
},
|
||||
removebehavior: async (interaction: AutocompleteInteraction) => {
|
||||
const focused = interaction.options.getFocused();
|
||||
|
@ -351,7 +351,7 @@ export default {
|
|||
.filter(b => b.behavior.name.toLowerCase().includes(focused.toLowerCase()));
|
||||
|
||||
return foundBehaviors.map(b => ({
|
||||
name: `${b.behavior.itemType}:${formatBehavior(b.behavior, b.value)} - ${b.behavior.description}`,
|
||||
name: `${b.behavior.type}:${formatBehavior(b.behavior, b.value)} - ${b.behavior.description}`,
|
||||
value: b.behavior.name
|
||||
}));
|
||||
},
|
||||
|
|
|
@ -41,7 +41,11 @@ export default {
|
|||
const behavior = getBehavior(itemBehavior.behavior);
|
||||
if (!behavior) continue;
|
||||
if (!behavior.onUse) continue;
|
||||
const res = await behavior.onUse(itemBehavior.value, item, member.id);
|
||||
const res = await behavior.onUse({
|
||||
value: itemBehavior.value,
|
||||
item,
|
||||
user: member.id
|
||||
});
|
||||
if (res instanceof Right) {
|
||||
await interaction.followUp(`You tried to use ${formatItem(item)}... but failed!\n${res.getValue()}`);
|
||||
return;
|
||||
|
|
|
@ -3,20 +3,32 @@ import { Either, Left, Right } from '../util';
|
|||
import { ItemBehavior, db } from '../db';
|
||||
import { BLOOD_ITEM, dealDamage } from './pvp';
|
||||
|
||||
interface BehaviorContext {
|
||||
value: number | undefined,
|
||||
}
|
||||
type ItemContext = BehaviorContext & {
|
||||
item: Item,
|
||||
user: string,
|
||||
}
|
||||
type AttackContext = ItemContext & {
|
||||
target: string,
|
||||
damage: number,
|
||||
}
|
||||
|
||||
export interface Behavior {
|
||||
name: string,
|
||||
description: string,
|
||||
itemType: 'plain' | 'weapon' | 'consumable',
|
||||
type: 'plain' | 'weapon' | 'consumable',
|
||||
// make it look fancy
|
||||
format?: (value?: number) => string,
|
||||
// triggers upon use
|
||||
// for 'weapons', this is on attack
|
||||
// for 'consumable' and `plain`, this is on use
|
||||
// returns Left upon success with an optional message, the reason for failure otherwise (Right)
|
||||
onUse?: (value: number | undefined, item: Item, user: string) => Promise<Either<string | null, string>>,
|
||||
onUse?: (ctx: ItemContext) => Promise<Either<string | null, string>>,
|
||||
// triggers upon `weapons` attack
|
||||
// returns the new damage value upon success (Left), the reason for failure otherwise (Right)
|
||||
onAttack?: (value: number | undefined, item: Item, user: string, target: string, damage: number) => Promise<Either<number, string>>,
|
||||
// returns the new damage value if applicable upon success and an optional message (Left), the reason for failure otherwise (Right)
|
||||
onAttack?: (ctx: AttackContext) => Promise<Either<{damage?: number, message?: string}, string>>,
|
||||
}
|
||||
|
||||
const defaultFormat = (behavior: Behavior, value?: number) => `${behavior.name}${value ? ' ' + value.toString() : ''}`;
|
||||
|
@ -25,52 +37,55 @@ export const behaviors: Behavior[] = [
|
|||
{
|
||||
name: 'heal',
|
||||
description: 'Heals the user by `value`',
|
||||
itemType: 'consumable',
|
||||
async onUse(value, item, user) {
|
||||
if (!value) return new Right('A value is required for this behavior');
|
||||
await dealDamage(user, -Math.floor(value));
|
||||
return new Left(`You were healed by ${formatItems(BLOOD_ITEM, value)}!`);
|
||||
type: 'consumable',
|
||||
async onUse(ctx) {
|
||||
if (!ctx.value) return new Right('A value is required for this behavior');
|
||||
await dealDamage(ctx.user, -Math.floor(ctx.value));
|
||||
return new Left(`You were healed by ${formatItems(BLOOD_ITEM, ctx.value)}!`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'damage',
|
||||
description: 'Damages the user by `value',
|
||||
itemType: 'consumable',
|
||||
async onUse(value, item, user) {
|
||||
if (!value) return new Right('A value is required for this behavior');
|
||||
await dealDamage(user, Math.floor(value));
|
||||
return new Left(`You were damaged by ${formatItems(BLOOD_ITEM, value)}!`);
|
||||
type: 'consumable',
|
||||
async onUse(ctx) {
|
||||
if (!ctx.value) return new Right('A value is required for this behavior');
|
||||
await dealDamage(ctx.user, Math.floor(ctx.value));
|
||||
return new Left(`You were damaged by ${formatItems(BLOOD_ITEM, ctx.value)}!`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'random_up',
|
||||
description: 'Randomizes the attack value up by a maximum of `value`',
|
||||
itemType: 'weapon',
|
||||
type: 'weapon',
|
||||
format: (value) => `random +${value}`,
|
||||
async onAttack(value, item, user, target, damage) {
|
||||
if (!value) return new Right('A value is required for this behavior');
|
||||
return new Left(damage + Math.round(Math.random() * value));
|
||||
async onAttack(ctx) {
|
||||
if (!ctx.value) return new Right('A value is required for this behavior');
|
||||
return new Left({ damage: ctx.damage + Math.round(Math.random() * ctx.value) });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'random_down',
|
||||
description: 'Randomizes the attack value down by a maximum of `value`',
|
||||
itemType: 'weapon',
|
||||
type: 'weapon',
|
||||
format: (value) => `random -${value}`,
|
||||
async onAttack(value, item, user, target, damage) {
|
||||
if (!value) return new Right('A value is required for this behavior');
|
||||
return new Left(damage - Math.round(Math.random() * value));
|
||||
async onAttack(ctx) {
|
||||
if (!ctx.value) return new Right('A value is required for this behavior');
|
||||
return new Left({ damage: ctx.damage - Math.round(Math.random() * ctx.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) {
|
||||
if (!value) return new Right('A value is required for this behavior');
|
||||
giveItem(user, BLOOD_ITEM, Math.floor(damage * value));
|
||||
return new Left(damage);
|
||||
type: 'weapon',
|
||||
format: (value) => `lifesteal ${value}x`,
|
||||
async onAttack(ctx) {
|
||||
if (!ctx.value) return new Right('A value is required for this behavior');
|
||||
const amt = Math.floor(ctx.damage * ctx.value);
|
||||
giveItem(ctx.user, BLOOD_ITEM, amt);
|
||||
return new Left({
|
||||
message: `You gained ${formatItems(BLOOD_ITEM, amt)} from your target!`
|
||||
});
|
||||
},
|
||||
}
|
||||
];
|
||||
|
|
|
@ -5,8 +5,8 @@ import { Client } from 'discord.js';
|
|||
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;
|
||||
const BLOOD_GAIN_PER_HOUR = 2;
|
||||
export const INVINCIBLE_TIMER = 1_000 * 60 * 30;
|
||||
|
||||
export async function initHealth(user: string) {
|
||||
const isInitialized = await db<InitHealth>('initHealth')
|
||||
|
|
Loading…
Reference in New Issue