behavior tweaks, rebalancing

This commit is contained in:
Jill 2023-11-25 19:21:19 +03:00
parent 1dee5ed060
commit 465c42437c
Signed by: oat
GPG Key ID: 33489AA58A955108
6 changed files with 92 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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!`
});
},
}
];

View File

@ -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')