Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Petfree #2510

Merged
merged 17 commits into from
Jul 11, 2024
2 changes: 1 addition & 1 deletion Core/src/commands/pet/PetCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default class PetCommand {
foundPet: true,
data: {
nickname: pet.nickname,
typeId: petModel.id,
petTypeId: petModel.id,
rarity: petModel.rarity,
sex: pet.sex,
loveLevel: pet.getLoveLevelNumber()
Expand Down
175 changes: 175 additions & 0 deletions Core/src/commands/pet/PetFreeCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {packetHandler} from "../../core/packetHandlers/PacketHandler";
import {WebsocketClient} from "../../../../Lib/src/instances/WebsocketClient";
import {DraftBotPacket, makePacket, PacketContext} from "../../../../Lib/src/packets/DraftBotPacket";
import {Player, Players} from "../../core/database/game/models/Player";
import {PetEntities, PetEntity} from "../../core/database/game/models/PetEntity";
import {
CommandPetFreeAcceptPacketRes,
CommandPetFreePacketReq,
CommandPetFreePacketRes,
CommandPetFreeRefusePacketRes
} from "../../../../Lib/src/packets/commands/CommandPetFreePacket";
import {BlockingUtils} from "../../core/utils/BlockingUtils";
import {PetFreeConstants} from "../../../../Lib/src/constants/PetFreeConstants";
import {EndCallback, ReactionCollectorInstance} from "../../core/utils/ReactionsCollector";
import {ReactionCollectorAcceptReaction} from "../../../../Lib/src/packets/interaction/ReactionCollectorPacket";
import {BlockingConstants} from "../../../../Lib/src/constants/BlockingConstants";
import {ReactionCollectorPetFree} from "../../../../Lib/src/packets/interaction/ReactionCollectorPetFree";
import {LogsDatabase} from "../../core/database/logs/LogsDatabase";
import {NumberChangeReason} from "../../../../Lib/src/constants/LogsConstants";
import Guild, {Guilds} from "../../core/database/game/models/Guild";
import {GuildConstants} from "../../../../Lib/src/constants/GuildConstants";
import {getFoodIndexOf} from "../../core/utils/FoodUtils";
import {RandomUtils} from "../../core/utils/RandomUtils";
import {Constants} from "../../../../Lib/src/constants/Constants";


/**
* Send the number of milliseconds that remain before the player is allowed to free his pet
* (a player cannot free pets too often)
* @param player
*/
function getCooldownRemainingTimeMs(player: Player): number {
return PetFreeConstants.FREE_COOLDOWN - (new Date().valueOf() - player.lastPetFree.valueOf());
}

/**
* Returns the amount of money the player is missing to free his pet (0 if the player has enough money)
* @param player
* @param playerPet
*/
function getMissingMoneyToFreePet(player: Player, playerPet: PetEntity): number {
return playerPet.isFeisty() ? PetFreeConstants.FREE_FEISTY_COST - player.money : 0;
}

/**
* Return true if the player is "lucky" and wins a meat piece for freeing his pet
* @param guild
* @param pPet
*/
function generateLuckyMeat(guild: Guild, pPet: PetEntity): boolean {
return guild && guild.carnivorousFood + 1 <= GuildConstants.MAX_PET_FOOD[getFoodIndexOf(Constants.PET_FOOD.CARNIVOROUS_FOOD)]
&& RandomUtils.draftbotRandom.realZeroToOneInclusive() <= PetFreeConstants.GIVE_MEAT_PROBABILITY
&& !pPet.isFeisty();
}

/**
* Accept the petfree request and free the pet
* @param player
* @param playerPet
* @param response
*/
async function acceptPetFree(player: Player, playerPet: PetEntity, response: DraftBotPacket[]): Promise<void> {
if (playerPet.isFeisty()) {
await player.addMoney({
amount: -PetFreeConstants.FREE_FEISTY_COST,
response,
reason: NumberChangeReason.PET_FREE
});
}

LogsDatabase.logPetFree(playerPet).then();

await playerPet.destroy();
player.petId = null;
player.lastPetFree = new Date();
await player.save();

let guild: Guild;
let luckyMeat = false;
try {
guild = await Guilds.getById(player.guildId);
luckyMeat = generateLuckyMeat(guild, playerPet);
if (luckyMeat) {
guild!.carnivorousFood += PetFreeConstants.MEAT_GIVEN;
await guild!.save();
}
}
catch (error) {
guild = null;
}

response.push(makePacket(CommandPetFreeAcceptPacketRes, {
petId: playerPet.typeId,
petSex: playerPet.sex,
petNickname: playerPet.nickname,
freeCost: playerPet.isFeisty() ? PetFreeConstants.FREE_FEISTY_COST : 0,
luckyMeat
}));
}

export default class PetFreeCommand {

@packetHandler(CommandPetFreePacketReq)
async execute(client: WebsocketClient, packet: CommandPetFreePacketReq, context: PacketContext, response: DraftBotPacket[]): Promise<void> {
const player = await Players.getByKeycloakId(packet.keycloakId);
if (BlockingUtils.appendBlockedPacket(player, response)) {
return;
}

const playerPet = await PetEntities.getById(player.petId);
if (!playerPet) {
response.push(makePacket(CommandPetFreePacketRes, {
foundPet: false
}));
return;
}

// Check cooldown
const cooldownRemainingTimeMs = getCooldownRemainingTimeMs(player);
if (cooldownRemainingTimeMs > 0) {
response.push(makePacket(CommandPetFreePacketRes, {
foundPet: true,
petCanBeFreed: false,
cooldownRemainingTimeMs
}));
return;
}

// Check money
const missingMoney = getMissingMoneyToFreePet(player, playerPet);
if (missingMoney > 0) {
response.push(makePacket(CommandPetFreePacketRes, {
foundPet: true,
petCanBeFreed: false,
missingMoney
}));
return;
}

// Send collector
const collector = new ReactionCollectorPetFree(
playerPet.typeId,
playerPet.sex,
playerPet.nickname,
playerPet.isFeisty() ? PetFreeConstants.FREE_FEISTY_COST : 0
);

const endCallback: EndCallback = async (collector: ReactionCollectorInstance, response: DraftBotPacket[]): Promise<void> => {
const reaction = collector.getFirstReaction();

if (reaction && reaction.reaction.type === ReactionCollectorAcceptReaction.name) {
await acceptPetFree(player, playerPet, response);
}
else {
response.push(makePacket(CommandPetFreeRefusePacketRes, {}));
}

BlockingUtils.unblockPlayer(player.id, BlockingConstants.REASONS.PET_FREE);
};

const collectorPacket = new ReactionCollectorInstance(
collector,
context,
{
allowedPlayerKeycloakIds: [player.keycloakId],
reactionLimit: 1
},
endCallback
)
.block(player.id, BlockingConstants.REASONS.PET_FREE)
.build();

response.push(collectorPacket);
}
}
4 changes: 4 additions & 0 deletions Core/src/core/database/game/models/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ export class Player extends Model {
return this;
}

/**
* Only for use when the player is spending money, this must not be used when the player is losing money
* @param parameters
*/
public async spendMoney(parameters: EditValueParameters): Promise<Player> {
await MissionsController.update(this, parameters.response, {
missionId: "spendMoney",
Expand Down
2 changes: 1 addition & 1 deletion Discord/src/commands/ICommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ICommand {
requirements: {
requiredLevel?: number,
disallowEffects?: Effect[],
allowEffects?: string[],
allowEffects?: Effect[],
userPermission?: string,
guildRequired?: boolean,
guildPermissions?: number
Expand Down
10 changes: 2 additions & 8 deletions Discord/src/commands/pet/PetCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,14 @@ export async function handleCommandPetPacketRes(packet: CommandPetPacketRes, con
new DraftBotErrorEmbed(
interaction.user,
interaction,
i18n.t("error:PetDoesntExist", {lng: interaction.userLanguage})
i18n.t("error:petDoesntExist", {lng: interaction.userLanguage})
)
]
});
return;
}

const petData: PetData = {
typeId: packet.data!.typeId,
nickname: packet.data!.nickname,
sex: packet.data!.sex,
rarity: packet.data!.rarity,
loveLevel: packet.data!.loveLevel
};
const petData: PetData = packet.data!;

const PetCommandEmbed = new DraftBotEmbed()
.formatAuthor(
Expand Down
146 changes: 146 additions & 0 deletions Discord/src/commands/pet/PetFreeCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {ICommand} from "../ICommand";
import {makePacket, PacketContext} from "../../../../Lib/src/packets/DraftBotPacket";
import {DraftbotInteraction} from "../../messages/DraftbotInteraction";
import i18n from "../../translations/i18n";
import {SlashCommandBuilderGenerator} from "../SlashCommandBuilderGenerator";
import {
CommandPetFreeAcceptPacketRes,
CommandPetFreePacketReq,
CommandPetFreePacketRes,
CommandPetFreeRefusePacketRes
} from "../../../../Lib/src/packets/commands/CommandPetFreePacket";
import {DiscordCache} from "../../bot/DiscordCache";
import {DraftBotErrorEmbed} from "../../messages/DraftBotErrorEmbed";
import {KeycloakUser} from "../../../../Lib/src/keycloak/KeycloakUser";
import {Effect} from "../../../../Lib/src/enums/Effect";
import {printTimeBeforeDate} from "../../../../Lib/src/utils/TimeUtils";
import {ReactionCollectorCreationPacket} from "../../../../Lib/src/packets/interaction/ReactionCollectorPacket";
import {DiscordCollectorUtils} from "../../utils/DiscordCollectorUtils";
import {DraftBotEmbed} from "../../messages/DraftBotEmbed";
import {ReactionCollectorPetFreeData} from "../../../../Lib/src/packets/interaction/ReactionCollectorPetFree";
import {PetUtils} from "../../utils/PetUtils";

/**
* Destroy a pet forever... RIP
*/
function getPacket(interaction: DraftbotInteraction, keycloakUser: KeycloakUser): CommandPetFreePacketReq {
return makePacket(CommandPetFreePacketReq, {keycloakId: keycloakUser.id});
}


export async function handleCommandPetFreePacketRes(packet: CommandPetFreePacketRes, context: PacketContext): Promise<void> {
const interaction = DiscordCache.getInteraction(context.discord!.interaction);
if (interaction) {
if (!packet.foundPet) {
await interaction.reply({
embeds: [
new DraftBotErrorEmbed(
interaction.user,
interaction,
i18n.t("error:petDoesntExist", {lng: interaction.userLanguage})
)
]
});
return;
}
if (!packet.petCanBeFreed) {
if (packet.missingMoney! > 0) {
await interaction.reply({
embeds: [
new DraftBotErrorEmbed(
interaction.user,
interaction,
i18n.t("error:notEnoughMoney", {
lng: interaction.userLanguage,
money: packet.missingMoney
})
)
]
});
}
if (packet.cooldownRemainingTimeMs! > 0) {
await interaction.reply({
embeds: [
new DraftBotErrorEmbed(
interaction.user,
interaction,
i18n.t("error:cooldownPetFree", {
lng: interaction.userLanguage,
remainingTime: printTimeBeforeDate(packet.cooldownRemainingTimeMs! + new Date().valueOf()),
interpolation: {escapeValue: false}
})
)
]
});
}
}
}
}

export async function createPetFreeCollector(packet: ReactionCollectorCreationPacket, context: PacketContext): Promise<void> {
const interaction = DiscordCache.getInteraction(context.discord!.interaction)!;
await interaction.deferReply();
const data = packet.data.data as ReactionCollectorPetFreeData;

const embed = new DraftBotEmbed().formatAuthor(i18n.t("commands:petFree.title", {
lng: interaction.userLanguage,
pseudo: interaction.user.displayName
}), interaction.user)
.setDescription(
i18n.t("commands:petFree.confirmDesc", {
lng: interaction.userLanguage,
pet: PetUtils.petToShortString(interaction.userLanguage, data.petNickname, data.petId, data.petSex)
})
);

await DiscordCollectorUtils.createAcceptRefuseCollector(interaction, embed, packet, context);
}

export async function handleCommandPetFreeRefusePacketRes(packet: CommandPetFreeRefusePacketRes, context: PacketContext): Promise<void> {
const originalInteraction = DiscordCache.getInteraction(context.discord!.interaction!);
const buttonInteraction = DiscordCache.getButtonInteraction(context.discord!.buttonInteraction!);
if (buttonInteraction && originalInteraction) {
await buttonInteraction.editReply({
embeds: [
new DraftBotEmbed().formatAuthor(i18n.t("commands:petFree.canceledTitle", {
lng: originalInteraction.userLanguage,
pseudo: originalInteraction.user.displayName
}), originalInteraction.user)
.setDescription(
i18n.t("commands:petFree.canceledDesc", {lng: originalInteraction.userLanguage})
)
.setErrorColor()
]
});
}
}

export async function handleCommandPetFreeAcceptPacketRes(packet: CommandPetFreeAcceptPacketRes, context: PacketContext): Promise<void> {
const originalInteraction = DiscordCache.getInteraction(context.discord!.interaction!);
const buttonInteraction = DiscordCache.getButtonInteraction(context.discord!.buttonInteraction!);
if (buttonInteraction && originalInteraction) {
await buttonInteraction.editReply({
embeds: [
new DraftBotEmbed().formatAuthor(i18n.t("commands:petFree.title", {
lng: originalInteraction.userLanguage,
pseudo: originalInteraction.user.displayName
}), originalInteraction.user)
.setDescription(
i18n.t("commands:petFree.acceptedDesc", {
lng: originalInteraction.userLanguage,
pet: PetUtils.petToShortString(originalInteraction.userLanguage, packet.petNickname, packet.petId, packet.petSex)
})
)
]
});
}
}

export const commandInfo: ICommand = {
slashCommandBuilder: SlashCommandBuilderGenerator.generateBaseCommand("petFree"),
getPacket,
requirements: {
allowEffects: [Effect.NO_EFFECT]
},
mainGuildCommand: false
};
5 changes: 2 additions & 3 deletions Discord/src/commands/player/ProfileCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,8 @@ function generateFields(packet: CommandProfilePacketRes, language: Language): Em
value: i18n.t("commands:profile.pet.fieldValue", {
lng: language,
emote: PetUtils.getPetIcon(packet.data.pet.typeId, packet.data.pet.sex),
rarity: PetUtils.getRarityDisplay(packet.data.pet.rarity),
nickname: packet.data.pet.nickname ?? PetUtils.getPetTypeName(language, packet.data.pet.typeId, packet.data.pet.sex)
}),
rarity: PetUtils.getRarityDisplay(packet.data.pet.rarity)
}) + PetUtils.petToShortString(language, packet.data.pet.nickname, packet.data.pet.typeId, packet.data.pet.sex),
inline: false
});
}
Expand Down
2 changes: 1 addition & 1 deletion Discord/src/messages/DraftbotSmallEventEmbed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class DraftbotSmallEventEmbed extends DraftBotEmbed {
constructor(smallEventId: keyof typeof DraftBotIcons.small_events, description: string, user: User, language: Language) {
super();
this.setAuthor({
name: i18n.t("commands:report.journal", { lng: language, pseudo: user.displayName }),
name: i18n.t("commands:report.journal", {lng: language, pseudo: user.displayName}),
iconURL: user.displayAvatarURL()
});
this.setDescription(`${DraftBotIcons.small_events[smallEventId]} ${description}`);
Expand Down
Loading
Loading