From 5315a09b4ea8b40ab56bed2dd6ac7cd065c9b76f Mon Sep 17 00:00:00 2001 From: Feluin Date: Sat, 19 Oct 2024 14:42:51 +0200 Subject: [PATCH 01/26] poc for fighting --- test_fight.ts | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test_fight.ts diff --git a/test_fight.ts b/test_fight.ts new file mode 100644 index 00000000..1d1e2a28 --- /dev/null +++ b/test_fight.ts @@ -0,0 +1,158 @@ +interface EquipableWeapon { + attack: Range; +} + +interface EquipableArmor { + defence: Range; + health: number; +} + +interface BaseEntity { + health: number; + name: string; + baseDamage: number; + baseDefence: number; + items: EquipableItem[]; + weapon?: EquipableWeapon; + armor?: EquipableArmor; + //TODO + permaBuffs?: undefined; +} + +function calcDamage(rawDamage: number, defence: number) { + if (defence >= rawDamage) { + return {damage: 0, mitigated: rawDamage}; + } + return {damage: rawDamage - defence, mitigated: defence}; + +} + +class Entity { + stats: BaseEntity; + + constructor(entity: BaseEntity) { + this.stats = entity; + if (this.stats.armor) { + this.stats.health += this.stats.armor?.health; + } + } + + attack(enemy: Entity) { + let rawDamage: number; + rawDamage = this.stats.baseDamage; + if (this.stats.weapon) { + rawDamage += randomValue(this.stats.weapon.attack); + } + this.stats.items.filter(value => value.attackModifier).forEach(value => + rawDamage += randomValue(value.attackModifier!) + ); + //TODO calc Items and Buffs for player + let defence = enemy.defend(); + let result = calcDamage(rawDamage, defence); + console.log(this.stats.name + " (" + this.stats.health + ") hits " + + enemy.stats.name + " (" + enemy.stats.health + ") for " + + result.damage + " mitigated " + result.mitigated); + enemy.stats.health -= result.damage; + } + + defend() { + let defence = this.stats.baseDefence; + if (this.stats.armor) { + defence += randomValue(this.stats.armor.defence); + } + this.stats.items.filter(value => value.defenceModifier).forEach(value => + defence += randomValue(value.defenceModifier!) + ); + return defence; + } + +} + +interface Range { + min: number; + max: number; +} + +interface EquipableItem { + name: string; + attackModifier?: Range; + defenceModifier?: Range; + afterFight?: (scene: FightScene) => void; + modifyAttack?: (scene: FightScene) => void; +} + + +interface FightScene { + player: Entity; + enemy: Entity; +} + +let exampleWeapon: EquipableWeapon = {attack: {min: 3, max: 8}}; +let exampleArmor: EquipableArmor = {defence: {min: 2, max: 5}, health: 20}; +let healitem: EquipableItem = { + name: "powerade", + afterFight: scene1 => { + if (Math.random() < 0.2) { + let health = randomValue({min: 1, max: 5}); + scene1.player.stats.health += health; + console.log(scene1.player.stats.name + " gets healed by " + health + " from item" + " powerade"); + } + } +}; +let playerstats = {name: "player", health: 80, baseDamage: 1, baseDefence: 0, items: [], weapon: exampleWeapon}; +let enemystats = { + name: "gudrun", + health: 120, + baseDamage: 1, + baseDefence: 1, + items: [healitem], + armor: exampleArmor +}; + +let enemy = new Entity(enemystats); +let player = new Entity(playerstats); + +let scene + : + FightScene = { + player: player, + enemy: enemy +}; +while (checkWin(scene) === undefined) { + //playerhit first + player.attack(enemy); + + // then enemny hit + enemy.attack(player); + //special effects from items + player.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight(scene); + }); + enemy.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight({player: enemy, enemy: player}); + }); +} + +type result = "PLAYER" | "ENEMY" | undefined; + +function checkWin(fightscene: FightScene): result { + if (fightscene.player.stats.health < 0) { + return "ENEMY"; + } + if (fightscene.enemy.stats.health < 0) { + return "PLAYER"; + } +} + +function randomValue(range: Range) { + return Math.round(range.min + Math.random() * (range.max - range.min)); +} + + +console.log(checkWin(scene)); From f92dfaaa41040f3e481da31f0ac45d29380a943e Mon Sep 17 00:00:00 2001 From: Feluin Date: Sat, 19 Oct 2024 14:42:51 +0200 Subject: [PATCH 02/26] poc for fighting --- test_fight.ts | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test_fight.ts diff --git a/test_fight.ts b/test_fight.ts new file mode 100644 index 00000000..1d1e2a28 --- /dev/null +++ b/test_fight.ts @@ -0,0 +1,158 @@ +interface EquipableWeapon { + attack: Range; +} + +interface EquipableArmor { + defence: Range; + health: number; +} + +interface BaseEntity { + health: number; + name: string; + baseDamage: number; + baseDefence: number; + items: EquipableItem[]; + weapon?: EquipableWeapon; + armor?: EquipableArmor; + //TODO + permaBuffs?: undefined; +} + +function calcDamage(rawDamage: number, defence: number) { + if (defence >= rawDamage) { + return {damage: 0, mitigated: rawDamage}; + } + return {damage: rawDamage - defence, mitigated: defence}; + +} + +class Entity { + stats: BaseEntity; + + constructor(entity: BaseEntity) { + this.stats = entity; + if (this.stats.armor) { + this.stats.health += this.stats.armor?.health; + } + } + + attack(enemy: Entity) { + let rawDamage: number; + rawDamage = this.stats.baseDamage; + if (this.stats.weapon) { + rawDamage += randomValue(this.stats.weapon.attack); + } + this.stats.items.filter(value => value.attackModifier).forEach(value => + rawDamage += randomValue(value.attackModifier!) + ); + //TODO calc Items and Buffs for player + let defence = enemy.defend(); + let result = calcDamage(rawDamage, defence); + console.log(this.stats.name + " (" + this.stats.health + ") hits " + + enemy.stats.name + " (" + enemy.stats.health + ") for " + + result.damage + " mitigated " + result.mitigated); + enemy.stats.health -= result.damage; + } + + defend() { + let defence = this.stats.baseDefence; + if (this.stats.armor) { + defence += randomValue(this.stats.armor.defence); + } + this.stats.items.filter(value => value.defenceModifier).forEach(value => + defence += randomValue(value.defenceModifier!) + ); + return defence; + } + +} + +interface Range { + min: number; + max: number; +} + +interface EquipableItem { + name: string; + attackModifier?: Range; + defenceModifier?: Range; + afterFight?: (scene: FightScene) => void; + modifyAttack?: (scene: FightScene) => void; +} + + +interface FightScene { + player: Entity; + enemy: Entity; +} + +let exampleWeapon: EquipableWeapon = {attack: {min: 3, max: 8}}; +let exampleArmor: EquipableArmor = {defence: {min: 2, max: 5}, health: 20}; +let healitem: EquipableItem = { + name: "powerade", + afterFight: scene1 => { + if (Math.random() < 0.2) { + let health = randomValue({min: 1, max: 5}); + scene1.player.stats.health += health; + console.log(scene1.player.stats.name + " gets healed by " + health + " from item" + " powerade"); + } + } +}; +let playerstats = {name: "player", health: 80, baseDamage: 1, baseDefence: 0, items: [], weapon: exampleWeapon}; +let enemystats = { + name: "gudrun", + health: 120, + baseDamage: 1, + baseDefence: 1, + items: [healitem], + armor: exampleArmor +}; + +let enemy = new Entity(enemystats); +let player = new Entity(playerstats); + +let scene + : + FightScene = { + player: player, + enemy: enemy +}; +while (checkWin(scene) === undefined) { + //playerhit first + player.attack(enemy); + + // then enemny hit + enemy.attack(player); + //special effects from items + player.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight(scene); + }); + enemy.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight({player: enemy, enemy: player}); + }); +} + +type result = "PLAYER" | "ENEMY" | undefined; + +function checkWin(fightscene: FightScene): result { + if (fightscene.player.stats.health < 0) { + return "ENEMY"; + } + if (fightscene.enemy.stats.health < 0) { + return "PLAYER"; + } +} + +function randomValue(range: Range) { + return Math.round(range.min + Math.random() * (range.max - range.min)); +} + + +console.log(checkWin(scene)); From e5d509526c47ac364a69a9e8cb3968cc93bb167e Mon Sep 17 00:00:00 2001 From: Feluin Date: Sat, 19 Oct 2024 19:28:06 +0200 Subject: [PATCH 03/26] added rudamentary visualisation --- src/commands/fight.ts | 30 ++++++ src/service/fight.ts | 212 ++++++++++++++++++++++++++++++++++++++++++ test_fight.ts | 158 ------------------------------- 3 files changed, 242 insertions(+), 158 deletions(-) create mode 100644 src/commands/fight.ts create mode 100644 src/service/fight.ts delete mode 100644 test_fight.ts diff --git a/src/commands/fight.ts b/src/commands/fight.ts new file mode 100644 index 00000000..123c71f0 --- /dev/null +++ b/src/commands/fight.ts @@ -0,0 +1,30 @@ +import type {ApplicationCommand} from "@/commands/command.js"; +import { + APIEmbed, APIEmbedField, + CommandInteraction, + ContextMenuCommandBuilder, + SlashCommandBuilder, + SlashCommandUserOption +} from "discord.js"; +import {BotContext} from "@/context.js"; +import {Entity, fight, FightScene} from "@/service/fight.js"; +import {JSONEncodable} from "@discordjs/util"; + + +export default class FightCommand implements ApplicationCommand { + + readonly description = "TBD"; + readonly name = "fight"; + readonly applicationCommand = new SlashCommandBuilder() + .setName(this.name) + .setDescription(this.description) + ; + + async handleInteraction(command: CommandInteraction, context: BotContext) { + + let interactionResponse = await command.deferReply(); + fight(interactionResponse); + } +} + + diff --git a/src/service/fight.ts b/src/service/fight.ts new file mode 100644 index 00000000..90d08c9a --- /dev/null +++ b/src/service/fight.ts @@ -0,0 +1,212 @@ +import {APIEmbed, APIEmbedField, BooleanCache, CacheType, InteractionResponse} from "discord.js"; +import {JSONEncodable} from "@discordjs/util"; +import {setTimeout} from "node:timers/promises"; +import {name} from "croner"; +import {en} from "chrono-node"; + +interface EquipableWeapon { + attack: Range; + name: string; +} + +interface EquipableArmor { + defence: Range; + health: number; + name: string; +} + +export interface BaseEntity { + health: number; + name: string; + baseDamage: number; + baseDefence: number; + items: EquipableItem[]; + weapon?: EquipableWeapon; + armor?: EquipableArmor; + //TODO + permaBuffs?: undefined; +} + +function calcDamage(rawDamage: number, defence: number) { + if (defence >= rawDamage) { + return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; + } + return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; + +} + +export class Entity { + stats: BaseEntity; + maxhealth: number; + lastattack?: number; + lastdefence?: number; + itemtext: string[] = []; + + constructor(entity: BaseEntity) { + this.stats = entity; + if (this.stats.armor) { + this.stats.health += this.stats.armor?.health; + } + this.maxhealth = this.stats.health; + } + + attack(enemy: Entity) { + let rawDamage: number; + rawDamage = this.stats.baseDamage; + if (this.stats.weapon) { + rawDamage += randomValue(this.stats.weapon.attack); + } + this.stats.items.filter(value => value.attackModifier).forEach(value => + rawDamage += randomValue(value.attackModifier!) + ); + let defence = enemy.defend(); + let result = calcDamage(rawDamage, defence); + console.log(this.stats.name + " (" + this.stats.health + ") hits " + + enemy.stats.name + " (" + enemy.stats.health + ") for " + + result.damage + " mitigated " + result.mitigated); + enemy.stats.health -= result.damage; + this.lastattack = result.rawDamage; + return result; + } + + defend() { + let defence = this.stats.baseDefence; + if (this.stats.armor) { + defence += randomValue(this.stats.armor.defence); + } + this.stats.items.filter(value => value.defenceModifier).forEach(value => + defence += randomValue(value.defenceModifier!) + ); + this.lastdefence = defence; + return defence; + } + +} + +interface Range { + min: number; + max: number; +} + +interface EquipableItem { + name: string; + attackModifier?: Range; + defenceModifier?: Range; + afterFight?: (scene: FightScene) => void; + modifyAttack?: (scene: FightScene) => void; +} + + +export interface FightScene { + player: Entity; + enemy: Entity; +} + +let exampleWeapon: EquipableWeapon = {name: "dildo", attack: {min: 3, max: 8}}; +let exampleArmor: EquipableArmor = {name: "nachthemd", defence: {min: 2, max: 5}, health: 20}; +let healitem: EquipableItem = { + name: "powerade", + afterFight: scene1 => { + if (Math.random() < 0.2) { + let health = randomValue({min: 1, max: 5}); + scene1.player.stats.health += health; + scene1.player.itemtext.push(healitem!.name + " +" + health + "HP"); + console.log(scene1.player.stats.name + " gets healed by " + health + " from item" + healitem.name); + } + } +}; + + +type result = "PLAYER" | "ENEMY" | undefined; + +function checkWin(fightscene: FightScene): result { + if (fightscene.player.stats.health < 0) { + return "ENEMY"; + } + if (fightscene.enemy.stats.health < 0) { + return "PLAYER"; + } +} + +function randomValue(range: Range) { + return Math.round(range.min + Math.random() * (range.max - range.min)); +} + +export async function fight(interactionResponse: InteractionResponse>) { + let playerstats = {name: "Player", health: 80, baseDamage: 1, baseDefence: 0, items: [], weapon: exampleWeapon}; + let enemystats = { + name: "Gudrun die Hexe", + health: 120, + baseDamage: 1, + baseDefence: 1, + items: [healitem], + armor: exampleArmor + }; + + let enemy = new Entity(enemystats); + let player = new Entity(playerstats); + + let scene + : + FightScene = { + player: player, + enemy: enemy + }; + while (checkWin(scene) === undefined) { + player.itemtext = []; + enemy.itemtext = []; + //playerhit first + player.attack(enemy); + // then enemny hit + enemy.attack(player); + //special effects from items + + player.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight(scene); + }); + enemy.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight({player: enemy, enemy: player}); + }); + await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); + await setTimeout(200); + } + +} + +function renderStats(player: Entity) { + while (player.itemtext.length < 5) { + player.itemtext.push("-") + } + + return { + name: player.stats.name, + value: + `❤️HP${player.stats.health}/${player.maxhealth} + ❤️${"=".repeat(Math.max(0, player.stats.health / player.maxhealth * 10))} + ⚔️Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastattack} + 🛡️Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastdefence} + 📚Items: + ${player.itemtext.join("\n")} + `, + inline: true + }; + +} + +function renderFightEmbedded(fightscene: FightScene): JSONEncodable | APIEmbed { + return { + title: `Kampf zwischen ${fightscene.player.stats.name} und ${fightscene.enemy.stats.name}`, + description: "Lol hier beschreibung", + fields: [renderStats(fightscene.player), renderStats(fightscene.enemy), { + name: "Verlauf", + value: " " + }] + }; +} + diff --git a/test_fight.ts b/test_fight.ts deleted file mode 100644 index 1d1e2a28..00000000 --- a/test_fight.ts +++ /dev/null @@ -1,158 +0,0 @@ -interface EquipableWeapon { - attack: Range; -} - -interface EquipableArmor { - defence: Range; - health: number; -} - -interface BaseEntity { - health: number; - name: string; - baseDamage: number; - baseDefence: number; - items: EquipableItem[]; - weapon?: EquipableWeapon; - armor?: EquipableArmor; - //TODO - permaBuffs?: undefined; -} - -function calcDamage(rawDamage: number, defence: number) { - if (defence >= rawDamage) { - return {damage: 0, mitigated: rawDamage}; - } - return {damage: rawDamage - defence, mitigated: defence}; - -} - -class Entity { - stats: BaseEntity; - - constructor(entity: BaseEntity) { - this.stats = entity; - if (this.stats.armor) { - this.stats.health += this.stats.armor?.health; - } - } - - attack(enemy: Entity) { - let rawDamage: number; - rawDamage = this.stats.baseDamage; - if (this.stats.weapon) { - rawDamage += randomValue(this.stats.weapon.attack); - } - this.stats.items.filter(value => value.attackModifier).forEach(value => - rawDamage += randomValue(value.attackModifier!) - ); - //TODO calc Items and Buffs for player - let defence = enemy.defend(); - let result = calcDamage(rawDamage, defence); - console.log(this.stats.name + " (" + this.stats.health + ") hits " + - enemy.stats.name + " (" + enemy.stats.health + ") for " - + result.damage + " mitigated " + result.mitigated); - enemy.stats.health -= result.damage; - } - - defend() { - let defence = this.stats.baseDefence; - if (this.stats.armor) { - defence += randomValue(this.stats.armor.defence); - } - this.stats.items.filter(value => value.defenceModifier).forEach(value => - defence += randomValue(value.defenceModifier!) - ); - return defence; - } - -} - -interface Range { - min: number; - max: number; -} - -interface EquipableItem { - name: string; - attackModifier?: Range; - defenceModifier?: Range; - afterFight?: (scene: FightScene) => void; - modifyAttack?: (scene: FightScene) => void; -} - - -interface FightScene { - player: Entity; - enemy: Entity; -} - -let exampleWeapon: EquipableWeapon = {attack: {min: 3, max: 8}}; -let exampleArmor: EquipableArmor = {defence: {min: 2, max: 5}, health: 20}; -let healitem: EquipableItem = { - name: "powerade", - afterFight: scene1 => { - if (Math.random() < 0.2) { - let health = randomValue({min: 1, max: 5}); - scene1.player.stats.health += health; - console.log(scene1.player.stats.name + " gets healed by " + health + " from item" + " powerade"); - } - } -}; -let playerstats = {name: "player", health: 80, baseDamage: 1, baseDefence: 0, items: [], weapon: exampleWeapon}; -let enemystats = { - name: "gudrun", - health: 120, - baseDamage: 1, - baseDefence: 1, - items: [healitem], - armor: exampleArmor -}; - -let enemy = new Entity(enemystats); -let player = new Entity(playerstats); - -let scene - : - FightScene = { - player: player, - enemy: enemy -}; -while (checkWin(scene) === undefined) { - //playerhit first - player.attack(enemy); - - // then enemny hit - enemy.attack(player); - //special effects from items - player.stats.items.forEach(value => { - if (!value.afterFight) { - return; - } - value.afterFight(scene); - }); - enemy.stats.items.forEach(value => { - if (!value.afterFight) { - return; - } - value.afterFight({player: enemy, enemy: player}); - }); -} - -type result = "PLAYER" | "ENEMY" | undefined; - -function checkWin(fightscene: FightScene): result { - if (fightscene.player.stats.health < 0) { - return "ENEMY"; - } - if (fightscene.enemy.stats.health < 0) { - return "PLAYER"; - } -} - -function randomValue(range: Range) { - return Math.round(range.min + Math.random() * (range.max - range.min)); -} - - -console.log(checkWin(scene)); From 978169516aacc88c95e3cc0cf87727847fdf38f5 Mon Sep 17 00:00:00 2001 From: Feluin Date: Sat, 19 Oct 2024 19:29:08 +0200 Subject: [PATCH 04/26] added rudamentary visualisation --- src/commands/fight.ts | 21 ++++----- src/service/fight.ts | 107 ++++++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 58 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 123c71f0..427ff8eb 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,30 +1,25 @@ -import type {ApplicationCommand} from "@/commands/command.js"; +import type { ApplicationCommand } from "@/commands/command.js"; import { - APIEmbed, APIEmbedField, + APIEmbed, + APIEmbedField, CommandInteraction, ContextMenuCommandBuilder, SlashCommandBuilder, - SlashCommandUserOption + SlashCommandUserOption, } from "discord.js"; -import {BotContext} from "@/context.js"; -import {Entity, fight, FightScene} from "@/service/fight.js"; -import {JSONEncodable} from "@discordjs/util"; - +import { BotContext } from "@/context.js"; +import { Entity, fight, FightScene } from "@/service/fight.js"; +import { JSONEncodable } from "@discordjs/util"; export default class FightCommand implements ApplicationCommand { - readonly description = "TBD"; readonly name = "fight"; readonly applicationCommand = new SlashCommandBuilder() .setName(this.name) - .setDescription(this.description) - ; + .setDescription(this.description); async handleInteraction(command: CommandInteraction, context: BotContext) { - let interactionResponse = await command.deferReply(); fight(interactionResponse); } } - - diff --git a/src/service/fight.ts b/src/service/fight.ts index 90d08c9a..3914637c 100644 --- a/src/service/fight.ts +++ b/src/service/fight.ts @@ -1,8 +1,8 @@ -import {APIEmbed, APIEmbedField, BooleanCache, CacheType, InteractionResponse} from "discord.js"; -import {JSONEncodable} from "@discordjs/util"; -import {setTimeout} from "node:timers/promises"; -import {name} from "croner"; -import {en} from "chrono-node"; +import { APIEmbed, APIEmbedField, BooleanCache, CacheType, InteractionResponse } from "discord.js"; +import { JSONEncodable } from "@discordjs/util"; +import { setTimeout } from "node:timers/promises"; +import { name } from "croner"; +import { en } from "chrono-node"; interface EquipableWeapon { attack: Range; @@ -29,10 +29,9 @@ export interface BaseEntity { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; + return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; } - return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; - + return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; } export class Entity { @@ -56,14 +55,24 @@ export class Entity { if (this.stats.weapon) { rawDamage += randomValue(this.stats.weapon.attack); } - this.stats.items.filter(value => value.attackModifier).forEach(value => - rawDamage += randomValue(value.attackModifier!) - ); + this.stats.items + .filter(value => value.attackModifier) + .forEach(value => (rawDamage += randomValue(value.attackModifier!))); let defence = enemy.defend(); let result = calcDamage(rawDamage, defence); - console.log(this.stats.name + " (" + this.stats.health + ") hits " + - enemy.stats.name + " (" + enemy.stats.health + ") for " - + result.damage + " mitigated " + result.mitigated); + console.log( + this.stats.name + + " (" + + this.stats.health + + ") hits " + + enemy.stats.name + + " (" + + enemy.stats.health + + ") for " + + result.damage + + " mitigated " + + result.mitigated, + ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; return result; @@ -74,13 +83,12 @@ export class Entity { if (this.stats.armor) { defence += randomValue(this.stats.armor.defence); } - this.stats.items.filter(value => value.defenceModifier).forEach(value => - defence += randomValue(value.defenceModifier!) - ); + this.stats.items + .filter(value => value.defenceModifier) + .forEach(value => (defence += randomValue(value.defenceModifier!))); this.lastdefence = defence; return defence; } - } interface Range { @@ -96,27 +104,31 @@ interface EquipableItem { modifyAttack?: (scene: FightScene) => void; } - export interface FightScene { player: Entity; enemy: Entity; } -let exampleWeapon: EquipableWeapon = {name: "dildo", attack: {min: 3, max: 8}}; -let exampleArmor: EquipableArmor = {name: "nachthemd", defence: {min: 2, max: 5}, health: 20}; +let exampleWeapon: EquipableWeapon = { name: "dildo", attack: { min: 3, max: 8 } }; +let exampleArmor: EquipableArmor = { name: "nachthemd", defence: { min: 2, max: 5 }, health: 20 }; let healitem: EquipableItem = { name: "powerade", afterFight: scene1 => { if (Math.random() < 0.2) { - let health = randomValue({min: 1, max: 5}); + let health = randomValue({ min: 1, max: 5 }); scene1.player.stats.health += health; scene1.player.itemtext.push(healitem!.name + " +" + health + "HP"); - console.log(scene1.player.stats.name + " gets healed by " + health + " from item" + healitem.name); + console.log( + scene1.player.stats.name + + " gets healed by " + + health + + " from item" + + healitem.name, + ); } - } + }, }; - type result = "PLAYER" | "ENEMY" | undefined; function checkWin(fightscene: FightScene): result { @@ -133,24 +145,29 @@ function randomValue(range: Range) { } export async function fight(interactionResponse: InteractionResponse>) { - let playerstats = {name: "Player", health: 80, baseDamage: 1, baseDefence: 0, items: [], weapon: exampleWeapon}; + let playerstats = { + name: "Player", + health: 80, + baseDamage: 1, + baseDefence: 0, + items: [], + weapon: exampleWeapon, + }; let enemystats = { name: "Gudrun die Hexe", health: 120, baseDamage: 1, baseDefence: 1, items: [healitem], - armor: exampleArmor + armor: exampleArmor, }; let enemy = new Entity(enemystats); let player = new Entity(playerstats); - let scene - : - FightScene = { + let scene: FightScene = { player: player, - enemy: enemy + enemy: enemy, }; while (checkWin(scene) === undefined) { player.itemtext = []; @@ -171,42 +188,42 @@ export async function fight(interactionResponse: InteractionResponse | APIEmbed { return { title: `Kampf zwischen ${fightscene.player.stats.name} und ${fightscene.enemy.stats.name}`, description: "Lol hier beschreibung", - fields: [renderStats(fightscene.player), renderStats(fightscene.enemy), { - name: "Verlauf", - value: " " - }] + fields: [ + renderStats(fightscene.player), + renderStats(fightscene.enemy), + { + name: "Verlauf", + value: " ", + }, + ], }; } - From cc81c090324e5b179104ccb692aab0889e675cd6 Mon Sep 17 00:00:00 2001 From: Feluin Date: Sat, 19 Oct 2024 21:15:29 +0200 Subject: [PATCH 05/26] added rudamentary visualisation --- src/commands/fight.ts | 6 +++--- src/service/fight.ts | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 427ff8eb..f5b6fedf 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -2,12 +2,12 @@ import type { ApplicationCommand } from "@/commands/command.js"; import { APIEmbed, APIEmbedField, - CommandInteraction, + type CommandInteraction, ContextMenuCommandBuilder, SlashCommandBuilder, SlashCommandUserOption, } from "discord.js"; -import { BotContext } from "@/context.js"; +import type { BotContext } from "@/context.js"; import { Entity, fight, FightScene } from "@/service/fight.js"; import { JSONEncodable } from "@discordjs/util"; @@ -19,7 +19,7 @@ export default class FightCommand implements ApplicationCommand { .setDescription(this.description); async handleInteraction(command: CommandInteraction, context: BotContext) { - let interactionResponse = await command.deferReply(); + const interactionResponse = await command.deferReply(); fight(interactionResponse); } } diff --git a/src/service/fight.ts b/src/service/fight.ts index 3914637c..6630a34b 100644 --- a/src/service/fight.ts +++ b/src/service/fight.ts @@ -1,5 +1,5 @@ -import { APIEmbed, APIEmbedField, BooleanCache, CacheType, InteractionResponse } from "discord.js"; -import { JSONEncodable } from "@discordjs/util"; +import { type APIEmbed, APIEmbedField, type BooleanCache, type CacheType, type InteractionResponse } from "discord.js"; +import type { JSONEncodable } from "@discordjs/util"; import { setTimeout } from "node:timers/promises"; import { name } from "croner"; import { en } from "chrono-node"; @@ -58,8 +58,8 @@ export class Entity { this.stats.items .filter(value => value.attackModifier) .forEach(value => (rawDamage += randomValue(value.attackModifier!))); - let defence = enemy.defend(); - let result = calcDamage(rawDamage, defence); + const defence = enemy.defend(); + const result = calcDamage(rawDamage, defence); console.log( this.stats.name + " (" + @@ -109,13 +109,13 @@ export interface FightScene { enemy: Entity; } -let exampleWeapon: EquipableWeapon = { name: "dildo", attack: { min: 3, max: 8 } }; -let exampleArmor: EquipableArmor = { name: "nachthemd", defence: { min: 2, max: 5 }, health: 20 }; -let healitem: EquipableItem = { +const exampleWeapon: EquipableWeapon = { name: "dildo", attack: { min: 3, max: 8 } }; +const exampleArmor: EquipableArmor = { name: "nachthemd", defence: { min: 2, max: 5 }, health: 20 }; +const healitem: EquipableItem = { name: "powerade", afterFight: scene1 => { if (Math.random() < 0.2) { - let health = randomValue({ min: 1, max: 5 }); + const health = randomValue({ min: 1, max: 5 }); scene1.player.stats.health += health; scene1.player.itemtext.push(healitem!.name + " +" + health + "HP"); console.log( @@ -145,7 +145,7 @@ function randomValue(range: Range) { } export async function fight(interactionResponse: InteractionResponse>) { - let playerstats = { + const playerstats = { name: "Player", health: 80, baseDamage: 1, @@ -153,7 +153,7 @@ export async function fight(interactionResponse: InteractionResponse Date: Sat, 19 Oct 2024 21:18:27 +0200 Subject: [PATCH 06/26] added rudamentary visualisation --- src/service/fight.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/service/fight.ts b/src/service/fight.ts index 6630a34b..e551d641 100644 --- a/src/service/fight.ts +++ b/src/service/fight.ts @@ -1,8 +1,7 @@ import { type APIEmbed, APIEmbedField, type BooleanCache, type CacheType, type InteractionResponse } from "discord.js"; import type { JSONEncodable } from "@discordjs/util"; import { setTimeout } from "node:timers/promises"; -import { name } from "croner"; -import { en } from "chrono-node"; + interface EquipableWeapon { attack: Range; From c1433ff0076b567a93e9cc12bc81e1780bdd1d37 Mon Sep 17 00:00:00 2001 From: Feluin Date: Tue, 22 Oct 2024 22:00:17 +0200 Subject: [PATCH 07/26] added some item --- src/commands/fight.ts | 32 ++++++- src/service/fight.ts | 157 +++------------------------------ src/service/fightData.ts | 183 +++++++++++++++++++++++++++++++++++++++ src/service/lootData.ts | 3 + src/storage/loot.ts | 4 +- 5 files changed, 231 insertions(+), 148 deletions(-) create mode 100644 src/service/fightData.ts diff --git a/src/commands/fight.ts b/src/commands/fight.ts index f5b6fedf..f515a04e 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -8,18 +8,44 @@ import { SlashCommandUserOption, } from "discord.js"; import type { BotContext } from "@/context.js"; -import { Entity, fight, FightScene } from "@/service/fight.js"; +import { fight, FightScene } from "@/service/fight.js"; import { JSONEncodable } from "@discordjs/util"; +import { bossMap, Entity } from "@/service/fightData.js"; export default class FightCommand implements ApplicationCommand { readonly description = "TBD"; readonly name = "fight"; readonly applicationCommand = new SlashCommandBuilder() .setName(this.name) - .setDescription(this.description); + .setDescription(this.description) + .addStringOption(builder => + builder + .setRequired(true) + .setName("boss") + .setDescription("Boss") + //switch to autocomplete when we reach 25 + .addChoices( + Object.entries(bossMap).map(boss => { + return { + name: boss[1].name, + value: boss[0], + }; + }), + ), + ); async handleInteraction(command: CommandInteraction, context: BotContext) { + const boss = command.options.get("boss", true).value as string; const interactionResponse = await command.deferReply(); - fight(interactionResponse); + const playerstats = { + name: "Player", + description: "", + health: 80, + baseDamage: 1, + baseDefence: 0, + items: [], + }; + console.log(boss); + await fight(playerstats, bossMap[boss], interactionResponse); } } diff --git a/src/service/fight.ts b/src/service/fight.ts index e551d641..926b5ea3 100644 --- a/src/service/fight.ts +++ b/src/service/fight.ts @@ -1,133 +1,19 @@ -import { type APIEmbed, APIEmbedField, type BooleanCache, type CacheType, type InteractionResponse } from "discord.js"; +import { + type APIEmbed, + APIEmbedField, + type BooleanCache, + type CacheType, + type InteractionResponse, +} from "discord.js"; import type { JSONEncodable } from "@discordjs/util"; import { setTimeout } from "node:timers/promises"; - - -interface EquipableWeapon { - attack: Range; - name: string; -} - -interface EquipableArmor { - defence: Range; - health: number; - name: string; -} - -export interface BaseEntity { - health: number; - name: string; - baseDamage: number; - baseDefence: number; - items: EquipableItem[]; - weapon?: EquipableWeapon; - armor?: EquipableArmor; - //TODO - permaBuffs?: undefined; -} - -function calcDamage(rawDamage: number, defence: number) { - if (defence >= rawDamage) { - return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; - } - return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; -} - -export class Entity { - stats: BaseEntity; - maxhealth: number; - lastattack?: number; - lastdefence?: number; - itemtext: string[] = []; - - constructor(entity: BaseEntity) { - this.stats = entity; - if (this.stats.armor) { - this.stats.health += this.stats.armor?.health; - } - this.maxhealth = this.stats.health; - } - - attack(enemy: Entity) { - let rawDamage: number; - rawDamage = this.stats.baseDamage; - if (this.stats.weapon) { - rawDamage += randomValue(this.stats.weapon.attack); - } - this.stats.items - .filter(value => value.attackModifier) - .forEach(value => (rawDamage += randomValue(value.attackModifier!))); - const defence = enemy.defend(); - const result = calcDamage(rawDamage, defence); - console.log( - this.stats.name + - " (" + - this.stats.health + - ") hits " + - enemy.stats.name + - " (" + - enemy.stats.health + - ") for " + - result.damage + - " mitigated " + - result.mitigated, - ); - enemy.stats.health -= result.damage; - this.lastattack = result.rawDamage; - return result; - } - - defend() { - let defence = this.stats.baseDefence; - if (this.stats.armor) { - defence += randomValue(this.stats.armor.defence); - } - this.stats.items - .filter(value => value.defenceModifier) - .forEach(value => (defence += randomValue(value.defenceModifier!))); - this.lastdefence = defence; - return defence; - } -} - -interface Range { - min: number; - max: number; -} - -interface EquipableItem { - name: string; - attackModifier?: Range; - defenceModifier?: Range; - afterFight?: (scene: FightScene) => void; - modifyAttack?: (scene: FightScene) => void; -} +import { BaseEntity, Entity } from "@/service/fightData.js"; export interface FightScene { player: Entity; enemy: Entity; } -const exampleWeapon: EquipableWeapon = { name: "dildo", attack: { min: 3, max: 8 } }; -const exampleArmor: EquipableArmor = { name: "nachthemd", defence: { min: 2, max: 5 }, health: 20 }; -const healitem: EquipableItem = { - name: "powerade", - afterFight: scene1 => { - if (Math.random() < 0.2) { - const health = randomValue({ min: 1, max: 5 }); - scene1.player.stats.health += health; - scene1.player.itemtext.push(healitem!.name + " +" + health + "HP"); - console.log( - scene1.player.stats.name + - " gets healed by " + - health + - " from item" + - healitem.name, - ); - } - }, -}; - type result = "PLAYER" | "ENEMY" | undefined; function checkWin(fightscene: FightScene): result { @@ -139,28 +25,11 @@ function checkWin(fightscene: FightScene): result { } } -function randomValue(range: Range) { - return Math.round(range.min + Math.random() * (range.max - range.min)); -} - -export async function fight(interactionResponse: InteractionResponse>) { - const playerstats = { - name: "Player", - health: 80, - baseDamage: 1, - baseDefence: 0, - items: [], - weapon: exampleWeapon, - }; - const enemystats = { - name: "Gudrun die Hexe", - health: 120, - baseDamage: 1, - baseDefence: 1, - items: [healitem], - armor: exampleArmor, - }; - +export async function fight( + playerstats: BaseEntity, + enemystats: BaseEntity, + interactionResponse: InteractionResponse>, +) { const enemy = new Entity(enemystats); const player = new Entity(playerstats); diff --git a/src/service/fightData.ts b/src/service/fightData.ts new file mode 100644 index 00000000..7a61e1b1 --- /dev/null +++ b/src/service/fightData.ts @@ -0,0 +1,183 @@ +import { FightScene } from "@/service/fight.js"; + +export const gameWeapons: { [name: string]: EquipableWeapon } = { + dildo: { + type: "weapon", + name: "Dildo", + attack: { min: 3, max: 9 }, + }, +}; +export const gameArmor: { [name: string]: EquipableArmor } = { + nachthemd: { + type: "armor", + name: "Nachthemd", + health: 50, + defence: { min: 2, max: 5 }, + }, + eierwaermer: { + type: "armor", + name: "Eierwaermer", + health: 30, + defence: { min: 3, max: 5 }, + }, +}; +export const gameItems: { [name: string]: EquipableItem } = { + ayran: { + type: "item", + name: "Ayran", + attackModifier: { min: 2, max: 3 }, + }, + oettinger: { + type: "item", + name: "Ötti", + attackModifier: { min: 1, max: 5 }, + defenceModifier: { min: -3, max: 0 }, + }, +}; +export const bossMap: { [name: string]: BaseEntity } = { + gudrun: { + name: "Gudrun die Hexe", + description: "", + health: 120, + baseDamage: 1, + baseDefence: 1, + armor: gameArmor.nachthemd, + weapon: gameWeapons.dildo, + items: [], + }, + deinchef: { + name: "Deinen Chef", + description: "", + health: 120, + baseDamage: 1, + baseDefence: 1, + items: [], + }, + schutzheiliger: { + name: "Schutzheiliger der Matjesverkäufer", + description: "", + health: 120, + baseDamage: 1, + baseDefence: 1, + items: [], + }, + rentner: { + name: "Reeeeeeentner", + description: "Runter von meinem Rasen, dein Auto muss da weg", + health: 200, + baseDamage: 3, + baseDefence: 5, + items: [], + }, +}; + +export type Equipable = EquipableWeapon | EquipableItem | EquipableArmor; + +interface EquipableWeapon { + type: "weapon"; + attack: Range; + name: string; +} + +interface EquipableArmor { + type: "armor"; + defence: Range; + health: number; + name: string; +} + +export interface BaseEntity { + health: number; + name: string; + description: string; + baseDamage: number; + baseDefence: number; + items: EquipableItem[]; + weapon?: EquipableWeapon; + armor?: EquipableArmor; + //TODO + permaBuffs?: undefined; +} + +export class Entity { + stats: BaseEntity; + maxhealth: number; + lastattack?: number; + lastdefence?: number; + itemtext: string[] = []; + + constructor(entity: BaseEntity) { + this.stats = entity; + if (this.stats.armor) { + this.stats.health += this.stats.armor?.health; + } + this.maxhealth = this.stats.health; + } + + attack(enemy: Entity) { + let rawDamage: number; + rawDamage = this.stats.baseDamage; + if (this.stats.weapon) { + rawDamage += randomValue(this.stats.weapon.attack); + } + this.stats.items + .filter(value => value.attackModifier) + .forEach(value => (rawDamage += randomValue(value.attackModifier!))); + const defence = enemy.defend(); + const result = calcDamage(rawDamage, defence); + console.log( + this.stats.name + + " (" + + this.stats.health + + ") hits " + + enemy.stats.name + + " (" + + enemy.stats.health + + ") for " + + result.damage + + " mitigated " + + result.mitigated, + ); + enemy.stats.health -= result.damage; + this.lastattack = result.rawDamage; + return result; + } + + defend() { + let defence = this.stats.baseDefence; + if (this.stats.armor) { + defence += randomValue(this.stats.armor.defence); + } + this.stats.items + .filter(value => value.defenceModifier) + .forEach(value => (defence += randomValue(value.defenceModifier!))); + this.lastdefence = defence; + return defence; + } +} + +export interface Range { + min: number; + max: number; +} +export interface RangeWithRandom {} + +interface EquipableItem { + type: "item"; + name: string; + attackModifier?: Range; + defenceModifier?: Range; + afterFight?: (scene: FightScene) => void; + modifyAttack?: (scene: FightScene) => void; +} + +function randomValue(range: Range) { + return Math.round(range.min + Math.random() * (range.max - range.min)); +} + +function calcDamage(rawDamage: number, defence: number) { + if (defence >= rawDamage) { + return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; + } + return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; +} diff --git a/src/service/lootData.ts b/src/service/lootData.ts index cc02678e..d5c44bb4 100644 --- a/src/service/lootData.ts +++ b/src/service/lootData.ts @@ -6,6 +6,7 @@ import { GuildMember, type Guild } from "discord.js"; import type { Loot, LootAttribute } from "@/storage/db/model.js"; import log from "@log"; +import { gameItems } from "@/service/fightData.js"; const ACHTUNG_NICHT_DROPBAR_WEIGHT_KG = 0; @@ -196,6 +197,7 @@ export const lootTemplates: LootTemplate[] = [ dropDescription: "Der gute von Müller", emote: "🥛", asset: "assets/loot/09-ayran.jpg", + gameEquip: gameItems.ayran, }, { id: LootKindId.PKV, @@ -285,6 +287,7 @@ export const lootTemplates: LootTemplate[] = [ dropDescription: "Ja dann Prost ne!", emote: "🍺", asset: "assets/loot/16-oettinger.jpg", + gameEquip: gameItems.oettinger, }, { id: LootKindId.ACHIEVEMENT, diff --git a/src/storage/loot.ts b/src/storage/loot.ts index cd8ed859..2d254e26 100644 --- a/src/storage/loot.ts +++ b/src/storage/loot.ts @@ -21,6 +21,7 @@ import type { import db from "@db"; import type { LootAttributeKindId } from "@/service/lootData.js"; +import { Equipable } from "@/service/fightData.js"; export type LootUseCommandInteraction = ChatInputCommandInteraction & { channel: GuildTextBasedChannel; @@ -36,7 +37,7 @@ export interface LootTemplate { emote?: string; excludeFromInventory?: boolean; effects?: string[]; - + gameEquip?: Equipable; onDrop?: ( context: BotContext, winner: GuildMember, @@ -128,6 +129,7 @@ export async function findOfUser(user: User, ctx = db()) { } export type LootWithAttributes = Loot & { attributes: Readonly[] }; + export async function findOfUserWithAttributes( user: User, ctx = db(), From 7f011fadf77b868a305af807d58f34af30b99de7 Mon Sep 17 00:00:00 2001 From: Feluin Date: Wed, 23 Oct 2024 10:38:08 +0200 Subject: [PATCH 08/26] =?UTF-8?q?add=20command=20ausr=C3=BCsten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/fight.ts | 93 ++++++++++++++++++++++-- src/commands/gegenstand.ts | 140 +++++++++++++++++++++++-------------- src/service/fight.ts | 95 ++----------------------- src/service/fightData.ts | 76 ++++++++++++-------- 4 files changed, 227 insertions(+), 177 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index f515a04e..b75263e1 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,16 +1,16 @@ import type { ApplicationCommand } from "@/commands/command.js"; import { APIEmbed, - APIEmbedField, + APIEmbedField, type BooleanCache, type CacheType, type CommandInteraction, - ContextMenuCommandBuilder, + ContextMenuCommandBuilder, type InteractionResponse, SlashCommandBuilder, - SlashCommandUserOption, + SlashCommandUserOption } from "discord.js"; import type { BotContext } from "@/context.js"; -import { fight, FightScene } from "@/service/fight.js"; import { JSONEncodable } from "@discordjs/util"; -import { bossMap, Entity } from "@/service/fightData.js"; +import {BaseEntity, bossMap, Entity} from "@/service/fightData.js"; +import {setTimeout} from "node:timers/promises"; export default class FightCommand implements ApplicationCommand { readonly description = "TBD"; @@ -49,3 +49,86 @@ export default class FightCommand implements ApplicationCommand { await fight(playerstats, bossMap[boss], interactionResponse); } } + + +type result = "PLAYER" | "ENEMY" | undefined; + +function checkWin(fightscene: FightScene): result { + if (fightscene.player.stats.health < 0) { + return "ENEMY"; + } + if (fightscene.enemy.stats.health < 0) { + return "PLAYER"; + } +} + +export async function fight( + playerstats: BaseEntity, + enemystats: BaseEntity, + interactionResponse: InteractionResponse> +) { + const enemy = new Entity(enemystats); + const player = new Entity(playerstats); + + const scene: FightScene = { + player: player, + enemy: enemy + }; + while (checkWin(scene) === undefined) { + player.itemtext = []; + enemy.itemtext = []; + //playerhit first + player.attack(enemy); + // then enemny hit + enemy.attack(player); + //special effects from items + + player.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight(scene); + }); + enemy.stats.items.forEach(value => { + if (!value.afterFight) { + return; + } + value.afterFight({player: enemy, enemy: player}); + }); + await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); + await setTimeout(200); + } +} + +function renderStats(player: Entity) { + while (player.itemtext.length < 5) { + player.itemtext.push("-"); + } + + return { + name: player.stats.name, + value: `❤️HP${player.stats.health}/${player.maxhealth} + ❤️${"=".repeat(Math.max(0, (player.stats.health / player.maxhealth) * 10))} + ⚔️Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastattack} + 🛡️Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastdefence} + 📚Items: + ${player.itemtext.join("\n")} + `, + inline: true + }; +} + +function renderFightEmbedded(fightscene: FightScene): JSONEncodable | APIEmbed { + return { + title: `Kampf zwischen ${fightscene.player.stats.name} und ${fightscene.enemy.stats.name}`, + description: fightscene.enemy.stats.description, + fields: [ + renderStats(fightscene.player), + renderStats(fightscene.enemy), + { + name: "Verlauf", + value: " " + } + ] + }; +} diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index 037d5344..f0c0ae53 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -7,21 +7,21 @@ import { type CommandInteraction, SlashCommandBuilder, SlashCommandStringOption, - SlashCommandSubcommandBuilder, + SlashCommandSubcommandBuilder } from "discord.js"; import * as sentry from "@sentry/bun"; -import type { BotContext } from "@/context.js"; -import type { ApplicationCommand } from "@/commands/command.js"; -import type { LootUseCommandInteraction } from "@/storage/loot.js"; +import type {BotContext} from "@/context.js"; +import type {ApplicationCommand} from "@/commands/command.js"; +import type {LootUseCommandInteraction} from "@/storage/loot.js"; import * as lootService from "@/service/loot.js"; import * as lootRoleService from "@/service/lootRoles.js"; -import { randomEntry } from "@/utils/arrayUtils.js"; -import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; +import {randomEntry} from "@/utils/arrayUtils.js"; +import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; import * as imageService from "@/service/image.js"; import * as lootDataService from "@/service/lootData.js"; -import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; +import {LootAttributeClassId, LootAttributeKindId, LootKindId} from "@/service/lootData.js"; import log from "@log"; @@ -35,7 +35,7 @@ export default class GegenstandCommand implements ApplicationCommand { .addSubcommand( new SlashCommandSubcommandBuilder() .setName("entsorgen") - .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes"), + .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes") ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -46,8 +46,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Der Gegenstand, über den du Informationen haben möchtest") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -58,9 +58,22 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du benutzen möchtest") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("ausrüsten") + .setDescription("Rüste einen gegenstand aus") + .addStringOption( + new SlashCommandStringOption() + .setRequired(true) + .setName("item") + .setDescription("Die Sau, die du ausrüsten möchtest") + .setAutocomplete(true) + ) ); +; async handleInteraction(interaction: CommandInteraction, context: BotContext) { const command = ensureChatInputCommand(interaction); @@ -75,6 +88,9 @@ export default class GegenstandCommand implements ApplicationCommand { case "benutzen": await this.#useItem(interaction, context); break; + case "ausrüsten": + await this.#equipItem(interaction, context); + break; default: throw new Error(`Unknown subcommand: "${subCommand}"`); } @@ -88,33 +104,33 @@ export default class GegenstandCommand implements ApplicationCommand { { description: "Es ist kein Wärter im Dienst. Das Tor ist zu. Du rennst dagegen. Opfer.", - color: 0xff0000, - }, - ], + color: 0xff0000 + } + ] }); return; } const wasteContents = await lootService.getUserLootsByTypeId( interaction.user.id, - LootKindId.RADIOACTIVE_WASTE, + LootKindId.RADIOACTIVE_WASTE ); if (wasteContents.length === 0) { await interaction.reply({ - content: "Du hast keinen Atommüll, den du in die Grube werfen kannst.", + content: "Du hast keinen Atommüll, den du in die Grube werfen kannst." }); return; } const sweetContent = await lootService.getUserLootsWithAttribute( interaction.user.id, - LootAttributeKindId.SWEET, + LootAttributeKindId.SWEET ); if (sweetContent.length === 0) { await interaction.reply({ - content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst.", + content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst." }); return; } @@ -126,7 +142,7 @@ export default class GegenstandCommand implements ApplicationCommand { const messages = [ `Du hast dem Wärter ${currentGuard} etwas Atommüll und etwas Süßes zum Naschen gegeben.`, `${currentGuard} hat sich über deinen Atommüll und die süßen Sachen gefreut.`, - `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.`, + `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.` ]; await interaction.reply({ @@ -135,11 +151,11 @@ export default class GegenstandCommand implements ApplicationCommand { title: "Atommüll entsorgt!", description: randomEntry(messages), footer: { - text: "Jetzt ist es das Problem des deutschen Steuerzahlers", + text: "Jetzt ist es das Problem des deutschen Steuerzahlers" }, - color: 0x00ff00, - }, - ], + color: 0x00ff00 + } + ] }); } @@ -156,7 +172,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const { item, template, attributes } = info; + const {item, template, attributes} = info; const effects = template.effects ?? []; @@ -186,14 +202,14 @@ export default class GegenstandCommand implements ApplicationCommand { const extraFields: (APIEmbedField | undefined)[] = [ template.onUse !== undefined - ? { name: "🔧 Benutzbar", value: "", inline: true } + ? {name: "🔧 Benutzbar", value: "", inline: true} : undefined, ...otherAttributes.map(attribute => ({ name: `${attribute.shortDisplay} ${attribute.displayName}`.trim(), value: "", - inline: true, - })), + inline: true + })) ]; await interaction.reply({ @@ -204,31 +220,31 @@ export default class GegenstandCommand implements ApplicationCommand { color: 0x00ff00, image: attachment ? { - url: "attachment://hero.gif", - width: 128, - } + url: "attachment://hero.gif", + width: 128 + } : undefined, fields: [ ...effects.map(value => ({ name: "⚡️ Effekt", value, - inline: true, + inline: true })), - ...extraFields.filter(e => e !== undefined), + ...extraFields.filter(e => e !== undefined) ], footer: { - text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim(), - }, - }, + text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim() + } + } ], files: attachment ? [ - { - name: "hero.gif", - attachment, - }, - ] - : [], + { + name: "hero.gif", + attachment + } + ] + : [] }); } @@ -245,12 +261,12 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const { item, template } = info; + const {item, template} = info; if (template.onUse === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht benutzt werden.", - ephemeral: true, + ephemeral: true }); return; } @@ -260,14 +276,14 @@ export default class GegenstandCommand implements ApplicationCommand { keepInInventory = await template.onUse( interaction as LootUseCommandInteraction, context, - item, + item ); } catch (error) { log.error(error, "Error while using item"); sentry.captureException(error); await interaction.reply({ content: "Beim Benutzen dieses Gegenstands ist ein Fehler aufgetreten.", - ephemeral: true, + ephemeral: true }); } @@ -286,7 +302,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (!item) { await interaction.reply({ content: "Diesen Gegensand hast du nicht.", - ephemeral: true, + ephemeral: true }); return; } @@ -295,19 +311,19 @@ export default class GegenstandCommand implements ApplicationCommand { if (!template) { await interaction.reply({ content: "Dieser Gegenstand ist unbekannt.", - ephemeral: true, + ephemeral: true }); return; } const attributes = await lootService.getLootAttributes(item.id); - return { item, template, attributes }; + return {item, template, attributes}; } async autocomplete(interaction: AutocompleteInteraction) { const subCommand = interaction.options.getSubcommand(true); - if (subCommand !== "info" && subCommand !== "benutzen") { + if (subCommand !== "info" && subCommand !== "benutzen" && subCommand !== "ausrüsten") { return; } @@ -343,10 +359,32 @@ export default class GegenstandCommand implements ApplicationCommand { typeof emote === "string" ? `${emote} ${item.displayName} (${item.id})` : `${item.displayName} (${item.id})`, - value: String(item.id), + value: String(item.id) }); } await interaction.respond(completions); } + + async #equipItem(interaction: CommandInteraction, context: BotContext) { + if (!interaction.isChatInputCommand()) { + throw new Error("Interaction is not a chat input command"); + } + if (!interaction.guild || !interaction.channel) { + return; + } + + const info = await this.#fetchItem(interaction); + if (!info) { + return; + } + const {item, template} = info; + if (template.gameEquip === undefined) { + await interaction.reply({ + content: "Dieser Gegenstand kann nicht ausgerüstet werden.", + ephemeral: true + }); + return; + } + } } diff --git a/src/service/fight.ts b/src/service/fight.ts index 926b5ea3..bfba1665 100644 --- a/src/service/fight.ts +++ b/src/service/fight.ts @@ -3,95 +3,8 @@ import { APIEmbedField, type BooleanCache, type CacheType, - type InteractionResponse, + type InteractionResponse } from "discord.js"; -import type { JSONEncodable } from "@discordjs/util"; -import { setTimeout } from "node:timers/promises"; -import { BaseEntity, Entity } from "@/service/fightData.js"; - -export interface FightScene { - player: Entity; - enemy: Entity; -} - -type result = "PLAYER" | "ENEMY" | undefined; - -function checkWin(fightscene: FightScene): result { - if (fightscene.player.stats.health < 0) { - return "ENEMY"; - } - if (fightscene.enemy.stats.health < 0) { - return "PLAYER"; - } -} - -export async function fight( - playerstats: BaseEntity, - enemystats: BaseEntity, - interactionResponse: InteractionResponse>, -) { - const enemy = new Entity(enemystats); - const player = new Entity(playerstats); - - const scene: FightScene = { - player: player, - enemy: enemy, - }; - while (checkWin(scene) === undefined) { - player.itemtext = []; - enemy.itemtext = []; - //playerhit first - player.attack(enemy); - // then enemny hit - enemy.attack(player); - //special effects from items - - player.stats.items.forEach(value => { - if (!value.afterFight) { - return; - } - value.afterFight(scene); - }); - enemy.stats.items.forEach(value => { - if (!value.afterFight) { - return; - } - value.afterFight({ player: enemy, enemy: player }); - }); - await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); - await setTimeout(200); - } -} - -function renderStats(player: Entity) { - while (player.itemtext.length < 5) { - player.itemtext.push("-"); - } - - return { - name: player.stats.name, - value: `❤️HP${player.stats.health}/${player.maxhealth} - ❤️${"=".repeat(Math.max(0, (player.stats.health / player.maxhealth) * 10))} - ⚔️Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastattack} - 🛡️Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastdefence} - 📚Items: - ${player.itemtext.join("\n")} - `, - inline: true, - }; -} - -function renderFightEmbedded(fightscene: FightScene): JSONEncodable | APIEmbed { - return { - title: `Kampf zwischen ${fightscene.player.stats.name} und ${fightscene.enemy.stats.name}`, - description: "Lol hier beschreibung", - fields: [ - renderStats(fightscene.player), - renderStats(fightscene.enemy), - { - name: "Verlauf", - value: " ", - }, - ], - }; -} +import type {JSONEncodable} from "@discordjs/util"; +import {setTimeout} from "node:timers/promises"; +import {BaseEntity, Entity} from "@/service/fightData.js"; diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 7a61e1b1..c6c5986e 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,57 +1,59 @@ -import { FightScene } from "@/service/fight.js"; +import {FightScene} from "@/commands/fight.js"; + export const gameWeapons: { [name: string]: EquipableWeapon } = { dildo: { type: "weapon", name: "Dildo", - attack: { min: 3, max: 9 }, - }, + attack: {min: 3, max: 9} + } }; export const gameArmor: { [name: string]: EquipableArmor } = { nachthemd: { type: "armor", name: "Nachthemd", health: 50, - defence: { min: 2, max: 5 }, + defence: {min: 2, max: 5} }, eierwaermer: { type: "armor", name: "Eierwaermer", health: 30, - defence: { min: 3, max: 5 }, - }, + defence: {min: 3, max: 5} + } }; export const gameItems: { [name: string]: EquipableItem } = { ayran: { type: "item", name: "Ayran", - attackModifier: { min: 2, max: 3 }, + attackModifier: {min: 2, max: 3} }, oettinger: { type: "item", name: "Ötti", - attackModifier: { min: 1, max: 5 }, - defenceModifier: { min: -3, max: 0 }, - }, + attackModifier: {min: 1, max: 5}, + defenceModifier: {min: -3, max: 0} + } }; export const bossMap: { [name: string]: BaseEntity } = { gudrun: { name: "Gudrun die Hexe", description: "", - health: 120, - baseDamage: 1, - baseDefence: 1, + health: 150, + baseDamage: 2, + baseDefence: 0, armor: gameArmor.nachthemd, weapon: gameWeapons.dildo, - items: [], + items: [] }, + deinchef: { name: "Deinen Chef", description: "", health: 120, baseDamage: 1, baseDefence: 1, - items: [], + items: [] }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -59,7 +61,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [], + items: [] }, rentner: { name: "Reeeeeeentner", @@ -67,8 +69,17 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 200, baseDamage: 3, baseDefence: 5, - items: [], + items: [] }, + barkeeper: { + name: "Barkeeper aus Nürnia", + description: "Nach deiner Reise durch den Schrank durch kommst du nach Nürnia, wo dich ein freundlicher Barkeeper dich anlächelt " + + "und dir ein Eimergroßes Fass Gettorade hinstellt. Deine nächste aufgabe ist es ihn im Wetttrinken zu besiegen", + health: 350, + baseDamage: 5, + baseDefence: 5, + items: [] + } }; export type Equipable = EquipableWeapon | EquipableItem | EquipableArmor; @@ -86,6 +97,11 @@ interface EquipableArmor { name: string; } +export interface FightScene { + player: Entity; + enemy: Entity; +} + export interface BaseEntity { health: number; name: string; @@ -127,16 +143,16 @@ export class Entity { const result = calcDamage(rawDamage, defence); console.log( this.stats.name + - " (" + - this.stats.health + - ") hits " + - enemy.stats.name + - " (" + - enemy.stats.health + - ") for " + - result.damage + - " mitigated " + - result.mitigated, + " (" + + this.stats.health + + ") hits " + + enemy.stats.name + + " (" + + enemy.stats.health + + ") for " + + result.damage + + " mitigated " + + result.mitigated ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -160,7 +176,6 @@ export interface Range { min: number; max: number; } -export interface RangeWithRandom {} interface EquipableItem { type: "item"; @@ -177,7 +192,8 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; + return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; } - return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; + return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; } + From 7b197ed7c4220b129220c5fdc4b6557562f449c6 Mon Sep 17 00:00:00 2001 From: Feluin Date: Thu, 28 Nov 2024 17:39:26 +0100 Subject: [PATCH 09/26] =?UTF-8?q?add=20command=20ausr=C3=BCsten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/fight.ts | 30 ++--- src/commands/gegenstand.ts | 109 +++++++++---------- src/service/fight.ts | 11 +- src/service/fightData.ts | 62 +++++------ src/service/lootData.ts | 77 ++++++------- src/storage/fightinventory.ts | 14 +++ src/storage/migrations/10-loot-attributes.ts | 3 +- src/storage/migrations/11-fightsystem.ts | 31 ++++++ 8 files changed, 187 insertions(+), 150 deletions(-) create mode 100644 src/storage/fightinventory.ts create mode 100644 src/storage/migrations/11-fightsystem.ts diff --git a/src/commands/fight.ts b/src/commands/fight.ts index b75263e1..eb5d210e 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,16 +1,19 @@ import type { ApplicationCommand } from "@/commands/command.js"; import { APIEmbed, - APIEmbedField, type BooleanCache, type CacheType, + APIEmbedField, + type BooleanCache, + type CacheType, type CommandInteraction, - ContextMenuCommandBuilder, type InteractionResponse, + ContextMenuCommandBuilder, + type InteractionResponse, SlashCommandBuilder, - SlashCommandUserOption + SlashCommandUserOption, } from "discord.js"; import type { BotContext } from "@/context.js"; import { JSONEncodable } from "@discordjs/util"; -import {BaseEntity, bossMap, Entity} from "@/service/fightData.js"; -import {setTimeout} from "node:timers/promises"; +import { BaseEntity, bossMap, Entity, FightScene } from "@/service/fightData.js"; +import { setTimeout } from "node:timers/promises"; export default class FightCommand implements ApplicationCommand { readonly description = "TBD"; @@ -50,7 +53,6 @@ export default class FightCommand implements ApplicationCommand { } } - type result = "PLAYER" | "ENEMY" | undefined; function checkWin(fightscene: FightScene): result { @@ -65,14 +67,14 @@ function checkWin(fightscene: FightScene): result { export async function fight( playerstats: BaseEntity, enemystats: BaseEntity, - interactionResponse: InteractionResponse> + interactionResponse: InteractionResponse>, ) { const enemy = new Entity(enemystats); const player = new Entity(playerstats); const scene: FightScene = { player: player, - enemy: enemy + enemy: enemy, }; while (checkWin(scene) === undefined) { player.itemtext = []; @@ -93,9 +95,9 @@ export async function fight( if (!value.afterFight) { return; } - value.afterFight({player: enemy, enemy: player}); + value.afterFight({ player: enemy, enemy: player }); }); - await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); + await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); await setTimeout(200); } } @@ -114,7 +116,7 @@ function renderStats(player: Entity) { 📚Items: ${player.itemtext.join("\n")} `, - inline: true + inline: true, }; } @@ -127,8 +129,8 @@ function renderFightEmbedded(fightscene: FightScene): JSONEncodable | renderStats(fightscene.enemy), { name: "Verlauf", - value: " " - } - ] + value: " ", + }, + ], }; } diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index f0c0ae53..e5f33a47 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -7,21 +7,21 @@ import { type CommandInteraction, SlashCommandBuilder, SlashCommandStringOption, - SlashCommandSubcommandBuilder + SlashCommandSubcommandBuilder, } from "discord.js"; import * as sentry from "@sentry/bun"; -import type {BotContext} from "@/context.js"; -import type {ApplicationCommand} from "@/commands/command.js"; -import type {LootUseCommandInteraction} from "@/storage/loot.js"; +import type { BotContext } from "@/context.js"; +import type { ApplicationCommand } from "@/commands/command.js"; +import type { LootUseCommandInteraction } from "@/storage/loot.js"; import * as lootService from "@/service/loot.js"; import * as lootRoleService from "@/service/lootRoles.js"; -import {randomEntry} from "@/utils/arrayUtils.js"; -import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; +import { randomEntry } from "@/utils/arrayUtils.js"; +import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; import * as imageService from "@/service/image.js"; import * as lootDataService from "@/service/lootData.js"; -import {LootAttributeClassId, LootAttributeKindId, LootKindId} from "@/service/lootData.js"; +import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; import log from "@log"; @@ -35,7 +35,7 @@ export default class GegenstandCommand implements ApplicationCommand { .addSubcommand( new SlashCommandSubcommandBuilder() .setName("entsorgen") - .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes") + .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes"), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -46,8 +46,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Der Gegenstand, über den du Informationen haben möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -58,8 +58,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du benutzen möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -70,10 +70,9 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du ausrüsten möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ); -; async handleInteraction(interaction: CommandInteraction, context: BotContext) { const command = ensureChatInputCommand(interaction); @@ -104,33 +103,33 @@ export default class GegenstandCommand implements ApplicationCommand { { description: "Es ist kein Wärter im Dienst. Das Tor ist zu. Du rennst dagegen. Opfer.", - color: 0xff0000 - } - ] + color: 0xff0000, + }, + ], }); return; } const wasteContents = await lootService.getUserLootsByTypeId( interaction.user.id, - LootKindId.RADIOACTIVE_WASTE + LootKindId.RADIOACTIVE_WASTE, ); if (wasteContents.length === 0) { await interaction.reply({ - content: "Du hast keinen Atommüll, den du in die Grube werfen kannst." + content: "Du hast keinen Atommüll, den du in die Grube werfen kannst.", }); return; } const sweetContent = await lootService.getUserLootsWithAttribute( interaction.user.id, - LootAttributeKindId.SWEET + LootAttributeKindId.SWEET, ); if (sweetContent.length === 0) { await interaction.reply({ - content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst." + content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst.", }); return; } @@ -142,7 +141,7 @@ export default class GegenstandCommand implements ApplicationCommand { const messages = [ `Du hast dem Wärter ${currentGuard} etwas Atommüll und etwas Süßes zum Naschen gegeben.`, `${currentGuard} hat sich über deinen Atommüll und die süßen Sachen gefreut.`, - `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.` + `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.`, ]; await interaction.reply({ @@ -151,11 +150,11 @@ export default class GegenstandCommand implements ApplicationCommand { title: "Atommüll entsorgt!", description: randomEntry(messages), footer: { - text: "Jetzt ist es das Problem des deutschen Steuerzahlers" + text: "Jetzt ist es das Problem des deutschen Steuerzahlers", }, - color: 0x00ff00 - } - ] + color: 0x00ff00, + }, + ], }); } @@ -172,7 +171,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const {item, template, attributes} = info; + const { item, template, attributes } = info; const effects = template.effects ?? []; @@ -202,14 +201,14 @@ export default class GegenstandCommand implements ApplicationCommand { const extraFields: (APIEmbedField | undefined)[] = [ template.onUse !== undefined - ? {name: "🔧 Benutzbar", value: "", inline: true} + ? { name: "🔧 Benutzbar", value: "", inline: true } : undefined, ...otherAttributes.map(attribute => ({ name: `${attribute.shortDisplay} ${attribute.displayName}`.trim(), value: "", - inline: true - })) + inline: true, + })), ]; await interaction.reply({ @@ -220,31 +219,31 @@ export default class GegenstandCommand implements ApplicationCommand { color: 0x00ff00, image: attachment ? { - url: "attachment://hero.gif", - width: 128 - } + url: "attachment://hero.gif", + width: 128, + } : undefined, fields: [ ...effects.map(value => ({ name: "⚡️ Effekt", value, - inline: true + inline: true, })), - ...extraFields.filter(e => e !== undefined) + ...extraFields.filter(e => e !== undefined), ], footer: { - text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim() - } - } + text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim(), + }, + }, ], files: attachment ? [ - { - name: "hero.gif", - attachment - } - ] - : [] + { + name: "hero.gif", + attachment, + }, + ] + : [], }); } @@ -261,12 +260,12 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const {item, template} = info; + const { item, template } = info; if (template.onUse === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht benutzt werden.", - ephemeral: true + ephemeral: true, }); return; } @@ -276,14 +275,14 @@ export default class GegenstandCommand implements ApplicationCommand { keepInInventory = await template.onUse( interaction as LootUseCommandInteraction, context, - item + item, ); } catch (error) { log.error(error, "Error while using item"); sentry.captureException(error); await interaction.reply({ content: "Beim Benutzen dieses Gegenstands ist ein Fehler aufgetreten.", - ephemeral: true + ephemeral: true, }); } @@ -302,7 +301,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (!item) { await interaction.reply({ content: "Diesen Gegensand hast du nicht.", - ephemeral: true + ephemeral: true, }); return; } @@ -311,14 +310,14 @@ export default class GegenstandCommand implements ApplicationCommand { if (!template) { await interaction.reply({ content: "Dieser Gegenstand ist unbekannt.", - ephemeral: true + ephemeral: true, }); return; } const attributes = await lootService.getLootAttributes(item.id); - return {item, template, attributes}; + return { item, template, attributes }; } async autocomplete(interaction: AutocompleteInteraction) { @@ -359,7 +358,7 @@ export default class GegenstandCommand implements ApplicationCommand { typeof emote === "string" ? `${emote} ${item.displayName} (${item.id})` : `${item.displayName} (${item.id})`, - value: String(item.id) + value: String(item.id), }); } @@ -378,11 +377,11 @@ export default class GegenstandCommand implements ApplicationCommand { if (!info) { return; } - const {item, template} = info; + const { item, template } = info; if (template.gameEquip === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht ausgerüstet werden.", - ephemeral: true + ephemeral: true, }); return; } diff --git a/src/service/fight.ts b/src/service/fight.ts index bfba1665..2b847dc9 100644 --- a/src/service/fight.ts +++ b/src/service/fight.ts @@ -1,10 +1 @@ -import { - type APIEmbed, - APIEmbedField, - type BooleanCache, - type CacheType, - type InteractionResponse -} from "discord.js"; -import type {JSONEncodable} from "@discordjs/util"; -import {setTimeout} from "node:timers/promises"; -import {BaseEntity, Entity} from "@/service/fightData.js"; +import { type APIEmbed, APIEmbedField } from "discord.js"; diff --git a/src/service/fightData.ts b/src/service/fightData.ts index c6c5986e..5b973f81 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,39 +1,36 @@ -import {FightScene} from "@/commands/fight.js"; - - export const gameWeapons: { [name: string]: EquipableWeapon } = { dildo: { type: "weapon", name: "Dildo", - attack: {min: 3, max: 9} - } + attack: { min: 3, max: 9 }, + }, }; export const gameArmor: { [name: string]: EquipableArmor } = { nachthemd: { type: "armor", name: "Nachthemd", health: 50, - defence: {min: 2, max: 5} + defence: { min: 2, max: 5 }, }, eierwaermer: { type: "armor", name: "Eierwaermer", health: 30, - defence: {min: 3, max: 5} - } + defence: { min: 3, max: 5 }, + }, }; export const gameItems: { [name: string]: EquipableItem } = { ayran: { type: "item", name: "Ayran", - attackModifier: {min: 2, max: 3} + attackModifier: { min: 2, max: 3 }, }, oettinger: { type: "item", name: "Ötti", - attackModifier: {min: 1, max: 5}, - defenceModifier: {min: -3, max: 0} - } + attackModifier: { min: 1, max: 5 }, + defenceModifier: { min: -3, max: 0 }, + }, }; export const bossMap: { [name: string]: BaseEntity } = { gudrun: { @@ -44,7 +41,7 @@ export const bossMap: { [name: string]: BaseEntity } = { baseDefence: 0, armor: gameArmor.nachthemd, weapon: gameWeapons.dildo, - items: [] + items: [], }, deinchef: { @@ -53,7 +50,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [] + items: [], }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -61,7 +58,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [] + items: [], }, rentner: { name: "Reeeeeeentner", @@ -69,17 +66,18 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 200, baseDamage: 3, baseDefence: 5, - items: [] + items: [], }, barkeeper: { name: "Barkeeper aus Nürnia", - description: "Nach deiner Reise durch den Schrank durch kommst du nach Nürnia, wo dich ein freundlicher Barkeeper dich anlächelt " + + description: + "Nach deiner Reise durch den Schrank durch kommst du nach Nürnia, wo dich ein freundlicher Barkeeper dich anlächelt " + "und dir ein Eimergroßes Fass Gettorade hinstellt. Deine nächste aufgabe ist es ihn im Wetttrinken zu besiegen", health: 350, baseDamage: 5, baseDefence: 5, - items: [] - } + items: [], + }, }; export type Equipable = EquipableWeapon | EquipableItem | EquipableArmor; @@ -143,16 +141,16 @@ export class Entity { const result = calcDamage(rawDamage, defence); console.log( this.stats.name + - " (" + - this.stats.health + - ") hits " + - enemy.stats.name + - " (" + - enemy.stats.health + - ") for " + - result.damage + - " mitigated " + - result.mitigated + " (" + + this.stats.health + + ") hits " + + enemy.stats.name + + " (" + + enemy.stats.health + + ") for " + + result.damage + + " mitigated " + + result.mitigated, ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -176,6 +174,7 @@ export interface Range { min: number; max: number; } +interface PermaBuff {} interface EquipableItem { type: "item"; @@ -192,8 +191,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; + return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; } - return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; + return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; } - diff --git a/src/service/lootData.ts b/src/service/lootData.ts index d5c44bb4..837f00e4 100644 --- a/src/service/lootData.ts +++ b/src/service/lootData.ts @@ -64,8 +64,9 @@ export enum LootAttributeKindId { /** * @remarks The index of an item must be equal to the `LootTypeId` enum value. */ -export const lootTemplates: LootTemplate[] = [ - { + +export const lootTemplateMap: { [id: string]: LootTemplate } = { + [LootKindId.NICHTS]: { id: LootKindId.NICHTS, weight: 32, displayName: "Nichts", @@ -74,7 +75,7 @@ export const lootTemplates: LootTemplate[] = [ asset: null, excludeFromInventory: true, }, - { + [LootKindId.KADSE]: { id: LootKindId.KADSE, weight: 4, displayName: "Niedliche Kadse", @@ -86,7 +87,7 @@ export const lootTemplates: LootTemplate[] = [ [LootAttributeKindId.RADIOACTIVE]: "assets/loot/attributes/01-kadse-verstrahlt.jpg", }, }, - { + [LootKindId.MESSERBLOCK]: { id: LootKindId.MESSERBLOCK, weight: 1, displayName: "Messerblock", @@ -95,7 +96,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🔪", asset: "assets/loot/02-messerblock.jpg", }, - { + [LootKindId.KUEHLSCHRANK]: { id: LootKindId.KUEHLSCHRANK, weight: 1, displayName: "Sehr teurer Kühlschrank", @@ -106,7 +107,7 @@ export const lootTemplates: LootTemplate[] = [ asset: "assets/loot/03-kuehlschrank.jpg", effects: ["Lässt Essen nicht schimmeln"], }, - { + [LootKindId.DOENER]: { id: LootKindId.DOENER, weight: 5, displayName: "Döner", @@ -115,7 +116,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🥙", asset: "assets/loot/04-doener.jpg", }, - { + [LootKindId.KINN]: { id: LootKindId.KINN, weight: 0.5, displayName: "Kinn", @@ -124,7 +125,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "👶", asset: "assets/loot/05-kinn.jpg", }, - { + [LootKindId.KRANKSCHREIBUNG]: { id: LootKindId.KRANKSCHREIBUNG, weight: 0.5, displayName: "Arbeitsunfähigkeitsbescheinigung", @@ -157,7 +158,7 @@ export const lootTemplates: LootTemplate[] = [ return false; }, }, - { + [LootKindId.WUERFELWURF]: { id: LootKindId.WUERFELWURF, weight: 5, displayName: "Würfelwurf", @@ -171,7 +172,7 @@ export const lootTemplates: LootTemplate[] = [ await rollService.rollInChannel(winner.user, channel, 1, 6); }, }, - { + [LootKindId.GESCHENK]: { id: LootKindId.GESCHENK, weight: 2, displayName: "Geschenk", @@ -189,7 +190,7 @@ export const lootTemplates: LootTemplate[] = [ return false; }, }, - { + [LootKindId.AYRAN]: { id: LootKindId.AYRAN, weight: 1, displayName: "Ayran", @@ -199,7 +200,7 @@ export const lootTemplates: LootTemplate[] = [ asset: "assets/loot/09-ayran.jpg", gameEquip: gameItems.ayran, }, - { + [LootKindId.PKV]: { id: LootKindId.PKV, weight: 1, displayName: "Private Krankenversicherung", @@ -209,7 +210,7 @@ export const lootTemplates: LootTemplate[] = [ asset: "assets/loot/10-pkv.jpg", effects: ["` +100% ` Chance auf AU 🟢"], }, - { + [LootKindId.TRICHTER]: { id: LootKindId.TRICHTER, weight: 1, displayName: "Trichter", @@ -218,7 +219,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":trichter:", asset: "assets/loot/11-trichter.jpg", }, - { + [LootKindId.GRAFIKKARTE]: { id: LootKindId.GRAFIKKARTE, weight: 1, displayName: "Grafikkarte aus der Zukunft", @@ -227,7 +228,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🖥️", asset: "assets/loot/12-grafikkarte.png", }, - { + [LootKindId.HAENDEDRUCK]: { id: LootKindId.HAENDEDRUCK, weight: 1, displayName: "Feuchter Händedruck", @@ -237,7 +238,7 @@ export const lootTemplates: LootTemplate[] = [ asset: "assets/loot/13-haendedruck.jpg", excludeFromInventory: true, }, - { + [LootKindId.ERLEUCHTUNG]: { id: LootKindId.ERLEUCHTUNG, weight: 1, displayName: "Erleuchtung", @@ -258,7 +259,7 @@ export const lootTemplates: LootTemplate[] = [ }); }, }, - { + [LootKindId.BAN]: { id: LootKindId.BAN, weight: 1, displayName: "Willkürban", @@ -279,7 +280,7 @@ export const lootTemplates: LootTemplate[] = [ ); }, }, - { + [LootKindId.OETTINGER]: { id: LootKindId.OETTINGER, weight: 1, displayName: "Oettinger", @@ -289,7 +290,7 @@ export const lootTemplates: LootTemplate[] = [ asset: "assets/loot/16-oettinger.jpg", gameEquip: gameItems.oettinger, }, - { + [LootKindId.ACHIEVEMENT]: { id: LootKindId.ACHIEVEMENT, weight: 1, displayName: "Achievement", @@ -298,7 +299,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🏆", asset: "assets/loot/17-achievement.png", }, - { + [LootKindId.GME_AKTIE]: { id: LootKindId.GME_AKTIE, weight: 5, displayName: "Wertlose GME-Aktie", @@ -307,7 +308,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "📉", asset: "assets/loot/18-gme.jpg", }, - { + [LootKindId.FERRIS]: { id: LootKindId.FERRIS, weight: 3, displayName: "Ferris", @@ -316,7 +317,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🦀", asset: "assets/loot/19-ferris.png", }, - { + [LootKindId.HOMEPOD]: { id: LootKindId.HOMEPOD, weight: 5, displayName: "HomePod", @@ -325,7 +326,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🍎", asset: "assets/loot/20-homepod.jpg", }, - { + [LootKindId.RADIOACTIVE_WASTE]: { id: LootKindId.RADIOACTIVE_WASTE, weight: 1, displayName: "Radioaktiver Müll", @@ -336,7 +337,7 @@ export const lootTemplates: LootTemplate[] = [ asset: "assets/loot/21-radioaktiver-muell.jpg", effects: ["` +5% ` Chance auf leeres Geschenk 🔴"], }, - { + [LootKindId.SAHNE]: { id: LootKindId.SAHNE, weight: 1, displayName: "Sprühsahne", @@ -345,7 +346,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":sahne:", asset: "assets/loot/22-sahne.jpg", }, - { + [LootKindId.AEHRE]: { id: LootKindId.AEHRE, weight: 1, displayName: "Ehre", @@ -360,7 +361,7 @@ export const lootTemplates: LootTemplate[] = [ await ehre.addPoints(winner.id, 1); }, }, - { + [LootKindId.CROWDSTRIKE]: { id: LootKindId.CROWDSTRIKE, weight: 1, displayName: "Crowdstrike Falcon", @@ -369,7 +370,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":eagle:", asset: "assets/loot/24-crowdstrike.jpg", }, - { + [LootKindId.POWERADE_BLAU]: { id: LootKindId.POWERADE_BLAU, weight: 1, displayName: "Blaue Powerade", @@ -377,7 +378,7 @@ export const lootTemplates: LootTemplate[] = [ dropDescription: "Erfrischend erquickend. Besonders mit Vodka. Oder Korn.", asset: "assets/loot/25-powerade-blau.jpg", }, - { + [LootKindId.GAULOISES_BLAU]: { id: LootKindId.GAULOISES_BLAU, weight: 1, displayName: "Gauloises Blau", @@ -387,7 +388,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🚬", asset: "assets/loot/26-gauloises-blau.png", }, - { + [LootKindId.MAXWELL]: { id: LootKindId.MAXWELL, weight: 1, displayName: "Maxwell", @@ -396,7 +397,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "😸", asset: "assets/loot/27-maxwell.gif", }, - { + [LootKindId.SCHICHTBEGINN_ASSE_2]: { id: LootKindId.SCHICHTBEGINN_ASSE_2, weight: 12, displayName: "Wärter Asse II", @@ -410,7 +411,7 @@ export const lootTemplates: LootTemplate[] = [ await lootRoles.startAsseGuardShift(context, winner, channel); }, }, - { + [LootKindId.DRECK]: { id: LootKindId.DRECK, weight: 7, displayName: "Ein Glas Dreck", @@ -419,7 +420,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":jar:", asset: "assets/loot/29-dirt.jpg", }, - { + [LootKindId.EI]: { id: LootKindId.EI, weight: 3, displayName: "Ei", @@ -429,7 +430,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":egg:", asset: "assets/loot/30-egg.jpg", }, - { + [LootKindId.BRAVO]: { id: LootKindId.BRAVO, weight: 2, displayName: "Bravo", @@ -438,7 +439,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":newspaper2:", asset: "assets/loot/31-bravo.jpg", }, - { + [LootKindId.VERSCHIMMELTER_DOENER]: { id: LootKindId.VERSCHIMMELTER_DOENER, weight: ACHTUNG_NICHT_DROPBAR_WEIGHT_KG, displayName: "Verschimmelter Döner", @@ -447,7 +448,7 @@ export const lootTemplates: LootTemplate[] = [ emote: "🥙", asset: null, }, - { + [LootKindId.THUNFISCHSHAKE]: { id: LootKindId.THUNFISCHSHAKE, weight: 2, displayName: "Thunfischshake", @@ -456,7 +457,7 @@ export const lootTemplates: LootTemplate[] = [ emote: ":baby_bottle:", asset: "assets/loot/32-thunfischshake.jpg", }, - { + [LootKindId.KAFFEEMUEHLE]: { id: LootKindId.KAFFEEMUEHLE, weight: 2, displayName: "Kaffeemühle", @@ -465,7 +466,9 @@ export const lootTemplates: LootTemplate[] = [ emote: ":coffee:", asset: "assets/loot/34-kaffeemuehle.jpg", }, -] as const; +}; + +export const lootTemplates: LootTemplate[] = Object.values(lootTemplateMap); /* Ideas: diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts new file mode 100644 index 00000000..06b33848 --- /dev/null +++ b/src/storage/fightinventory.ts @@ -0,0 +1,14 @@ +import type {User} from "discord.js"; +import db from "@db"; + + +export async function getFightInventory(userId: User["id"], lootKindId: number, ctx = db()) { + return await ctx + .selectFrom("fightinventory") + .where("userid", "=", userId) + .selectAll() + .execute(); +} + +export async function + diff --git a/src/storage/migrations/10-loot-attributes.ts b/src/storage/migrations/10-loot-attributes.ts index f5eae9c3..f218a4f3 100644 --- a/src/storage/migrations/10-loot-attributes.ts +++ b/src/storage/migrations/10-loot-attributes.ts @@ -10,7 +10,6 @@ export async function up(db: Kysely) { .addColumn("displayName", "text", c => c.notNull()) .addColumn("shortDisplay", "text", c => c.notNull()) .addColumn("color", "integer") - .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .addColumn("deletedAt", "timestamp") @@ -65,7 +64,7 @@ export async function up(db: Kysely) { } } -function createUpdatedAtTrigger(db: Kysely, tableName: string) { +export function createUpdatedAtTrigger(db: Kysely, tableName: string) { return sql .raw(` create trigger ${tableName}_updatedAt diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts new file mode 100644 index 00000000..fe7d88fa --- /dev/null +++ b/src/storage/migrations/11-fightsystem.ts @@ -0,0 +1,31 @@ +import { sql, type Kysely } from "kysely"; +import { createUpdatedAtTrigger } from "@/storage/migrations/10-loot-attributes.js"; + +export async function up(db: Kysely) { + await db.schema + .createTable("fightinventory") + .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) + .addColumn("gameType", "text", c => c.notNull()) + .addColumn("gameTemplateName", "text", c => c.notNull()) + .addColumn("userid", "text") + .addColumn("lootId", "integer", c => c.references("loot.id").onDelete("cascade")) + .addColumn("equipped", "boolean", c => c.notNull()) + .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .execute(); + await createUpdatedAtTrigger(db, "fightinventory"); + await db.schema + .createTable("fighthistory") + .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) + .addColumn("userid", "text", c => c.notNull()) + .addColumn("bossName", "text", c => c.notNull()) + .addColumn("firsttime", "boolean", c => c.notNull()) + .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .execute(); + await createUpdatedAtTrigger(db, "fighthistory"); +} + +export async function down(_db: Kysely) { + throw new Error("Not supported lol"); +} From 3d24dd031f4c28d059d21b35cfa35f290a224281 Mon Sep 17 00:00:00 2001 From: Feluin Date: Fri, 13 Dec 2024 15:06:27 +0100 Subject: [PATCH 10/26] added kampfinventar --- src/commands/fight.ts | 18 +++--- src/commands/gegenstand.ts | 19 ++++++- src/commands/inventar.ts | 71 +++++++++++++++++++----- src/commands/stempelkarte.ts | 2 +- src/service/fightData.ts | 5 +- src/service/loot.ts | 5 +- src/service/lootDrop.ts | 20 +++++++ src/storage/db/model.ts | 38 ++++++++++++- src/storage/fightinventory.ts | 66 +++++++++++++++++++++- src/storage/loot.ts | 2 +- src/storage/migrations/11-fightsystem.ts | 11 ++-- 11 files changed, 217 insertions(+), 40 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index eb5d210e..0079e321 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,6 +1,6 @@ import type { ApplicationCommand } from "@/commands/command.js"; import { - APIEmbed, + type APIEmbed, APIEmbedField, type BooleanCache, type CacheType, @@ -11,8 +11,8 @@ import { SlashCommandUserOption, } from "discord.js"; import type { BotContext } from "@/context.js"; -import { JSONEncodable } from "@discordjs/util"; -import { BaseEntity, bossMap, Entity, FightScene } from "@/service/fightData.js"; +import type { JSONEncodable } from "@discordjs/util"; +import { type BaseEntity, bossMap, Entity, type FightScene } from "@/service/fightData.js"; import { setTimeout } from "node:timers/promises"; export default class FightCommand implements ApplicationCommand { @@ -85,18 +85,18 @@ export async function fight( enemy.attack(player); //special effects from items - player.stats.items.forEach(value => { + for (const value of player.stats.items) { if (!value.afterFight) { - return; + continue; } value.afterFight(scene); - }); - enemy.stats.items.forEach(value => { + } + for (const value of enemy.stats.items) { if (!value.afterFight) { - return; + continue; } value.afterFight({ player: enemy, enemy: player }); - }); + } await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); await setTimeout(200); } diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index e5f33a47..61570f8c 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -24,6 +24,7 @@ import * as lootDataService from "@/service/lootData.js"; import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; import log from "@log"; +import { equipItembyLoot } from "@/storage/fightinventory.js"; export default class GegenstandCommand implements ApplicationCommand { name = "gegenstand"; @@ -69,7 +70,7 @@ export default class GegenstandCommand implements ApplicationCommand { new SlashCommandStringOption() .setRequired(true) .setName("item") - .setDescription("Die Sau, die du ausrüsten möchtest") + .setDescription("Rüste dich für deinen nächsten Kampf") .setAutocomplete(true), ), ); @@ -350,6 +351,9 @@ export default class GegenstandCommand implements ApplicationCommand { if (subCommand === "benutzen" && template.onUse === undefined) { continue; } + if (subCommand === "ausrüsten" && template.gameEquip === undefined) { + continue; + } const emote = lootDataService.getEmote(interaction.guild, item); completions.push({ @@ -378,6 +382,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } const { item, template } = info; + log.info(item); if (template.gameEquip === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht ausgerüstet werden.", @@ -385,5 +390,17 @@ export default class GegenstandCommand implements ApplicationCommand { }); return; } + const result = await equipItembyLoot(interaction.user.id, item.id); + log.info(result); + const message = + result.unequipped.length == 0 + ? "Du hast " + result.equipped?.displayName + " ausgerüstet" + : "Du hast " + + result.unequipped.join(", ") + + " abgelegt und dafür " + + "" + + result.equipped?.displayName + + " ausgerüstet"; + await interaction.reply(message); } } diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index 393d0b0b..f46cd1d8 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -1,13 +1,12 @@ import { - ActionRowBuilder, type APIEmbed, - ButtonBuilder, ButtonStyle, type CacheType, type CommandInteraction, ComponentType, - SlashCommandBooleanOption, + type InteractionReplyOptions, SlashCommandBuilder, + SlashCommandStringOption, SlashCommandUserOption, type User, } from "discord.js"; @@ -18,9 +17,10 @@ import * as lootService from "@/service/loot.js"; import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; import { format } from "@/utils/stringUtils.js"; import * as lootDataService from "@/service/lootData.js"; -import { LootKindId, LootAttributeKindId } from "@/service/lootData.js"; +import { LootAttributeKindId } from "@/service/lootData.js"; import log from "@log"; +import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; export default class InventarCommand implements ApplicationCommand { name = "inventar"; @@ -35,18 +35,23 @@ export default class InventarCommand implements ApplicationCommand { .setName("user") .setDescription("Wem du tun willst"), ) - .addBooleanOption( - new SlashCommandBooleanOption() - .setName("long") + .addStringOption( + new SlashCommandStringOption() + .setName("typ") + .setDescription("Anzeige") .setRequired(false) - .setDescription("kurz oder lang"), + .addChoices( + { name: "Kampfausrüstung", value: "fightinventory" }, + { name: "Kurzfassung", value: "short" }, + { name: "Detailansicht", value: "long" }, + ), ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { const cmd = ensureChatInputCommand(interaction); const user = cmd.options.getUser("user") ?? cmd.user; - const long = cmd.options.getBoolean("long") ?? true; + const type = cmd.options.getString("typ") ?? "short"; const contents = await lootService.getInventoryContents(user); if (contents.length === 0) { @@ -55,12 +60,16 @@ export default class InventarCommand implements ApplicationCommand { }); return; } - - if (long) { - await this.#createLongEmbed(context, interaction, user); - return; + switch (type) { + case "long": + await this.#createLongEmbed(context, interaction, user); + return; + case "fightinventory": + await this.#createFightEmbed(context, interaction, user); + return; + default: + await this.#createShortEmbed(context, interaction, user); } - await this.#createShortEmbed(context, interaction, user); } async #createShortEmbed( @@ -219,4 +228,38 @@ export default class InventarCommand implements ApplicationCommand { }); }); } + + async #createFightEmbed(context: BotContext, interaction: CommandInteraction, user: User) { + const fightinventory = await getFightInventoryEnriched(user.id); + const display = { + title: `Kampfausrüstung von ${user.displayName}`, + description: + "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", + thumbnail: user.avatarURL() ? { url: user.avatarURL()! } : undefined, + fields: [ + { name: "Waffe", value: fightinventory.weapon?.item?.displayName ?? "Nix" }, + { name: "Rüstung", value: fightinventory.armor?.item?.displayName ?? "Nackt" }, + ...fightinventory.items.map(item => { + return { + name: item.item!.displayName, + value: "Hier sollten die buffs stehen", + inline: true, + }; + }), + { name: "Buffs", value: "Nix" }, + ], + footer: { + text: `Lol ist noch nicht fertig`, + }, + } satisfies APIEmbed; + + const embed = { + components: [], + embeds: [display], + fetchReply: true, + tts: false, + } as const satisfies InteractionReplyOptions; + + const message = await interaction.reply(embed); + } } diff --git a/src/commands/stempelkarte.ts b/src/commands/stempelkarte.ts index 9f50558c..bd6c85ef 100644 --- a/src/commands/stempelkarte.ts +++ b/src/commands/stempelkarte.ts @@ -43,7 +43,7 @@ const firmenstempelCenter = { y: 155, }; -const getAvatarUrlForMember = (member?: GuildMember, size: ImageSize = 32) => { +export const getAvatarUrlForMember = (member?: GuildMember, size: ImageSize = 32) => { return ( member?.user.avatarURL({ size, diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 5b973f81..2a9ef5ca 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -80,6 +80,8 @@ export const bossMap: { [name: string]: BaseEntity } = { }, }; +export type FightItemType = "weapon" | "armor" | "item"; + export type Equipable = EquipableWeapon | EquipableItem | EquipableArmor; interface EquipableWeapon { @@ -174,7 +176,8 @@ export interface Range { min: number; max: number; } -interface PermaBuff {} + +type PermaBuff = {}; interface EquipableItem { type: "item"; diff --git a/src/service/loot.ts b/src/service/loot.ts index e1afe7a3..d85a5458 100644 --- a/src/service/loot.ts +++ b/src/service/loot.ts @@ -4,6 +4,7 @@ import type { LootId, LootInsertable, LootOrigin } from "@/storage/db/model.js"; import type { LootAttributeKindId, LootKindId } from "./lootData.js"; import * as loot from "@/storage/loot.js"; import * as lootDataService from "@/service/lootData.js"; +import db from "@db"; export async function getInventoryContents(user: User) { const contents = await loot.findOfUserWithAttributes(user); @@ -24,8 +25,8 @@ export async function getUserLootsWithAttribute( return await loot.getUserLootsWithAttribute(userId, attributeKindId); } -export async function getUserLootById(userId: Snowflake, id: LootId) { - return await loot.getUserLootById(userId, id); +export async function getUserLootById(userId: Snowflake, id: LootId, ctx = db()) { + return await loot.getUserLootById(userId, id, ctx); } export async function getLootAttributes(id: LootId) { diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index cb4b1a29..1fc52401 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -152,6 +152,26 @@ export async function postLootDrop( return; } + for (let i = 0; i < 50; i++) { + const defaultWeights = lootTemplates.map(t => t.weight); + const { messages, weights } = await getDropWeightAdjustments( + interaction.user, + defaultWeights, + ); + + const rarityWeights = lootAttributeTemplates.map(a => a.initialDropWeight ?? 0); + const initialAttribute = randomEntryWeighted(lootAttributeTemplates, rarityWeights); + + const template = randomEntryWeighted(lootTemplates, weights); + const claimedLoot = await lootService.createLoot( + template, + interaction.user, + message, + "drop", + predecessorLootId ?? null, + initialAttribute, + ); + } const defaultWeights = lootTemplates.map(t => t.weight); const { messages, weights } = await getDropWeightAdjustments(interaction.user, defaultWeights); diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index 673cbb4c..ea3b9da8 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -24,6 +24,8 @@ export interface Database { lootAttribute: LootAttributeTable; emote: EmoteTable; emoteUse: EmoteUseTable; + fighthistory: FightHistoryTable; + fightinventory: FightInventoryTable; } export type OneBasedMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; @@ -34,15 +36,17 @@ export interface AuditedTable { } export type Birthday = Selectable; + export interface BirthdayTable extends AuditedTable { id: GeneratedAlways; - userId: Snowflake; + userid: Snowflake; month: OneBasedMonth; day: number; } export type Stempel = Selectable; + export interface StempelTable extends AuditedTable { id: GeneratedAlways; @@ -51,6 +55,7 @@ export interface StempelTable extends AuditedTable { } export type SplidLink = Selectable; + export interface SplidLinkTable extends AuditedTable { id: GeneratedAlways; @@ -64,6 +69,7 @@ export interface SplidLinkTable extends AuditedTable { } export type SplidGroup = Selectable; + export interface SplidGroupTable extends AuditedTable { id: GeneratedAlways; @@ -77,6 +83,7 @@ export interface SplidGroupTable extends AuditedTable { } export type GuildRagequit = Selectable; + export interface GuildRagequitTable extends AuditedTable { id: GeneratedAlways; @@ -86,6 +93,7 @@ export interface GuildRagequitTable extends AuditedTable { } export type NickName = Selectable; + export interface NickNameTable extends AuditedTable { id: GeneratedAlways; @@ -94,6 +102,7 @@ export interface NickNameTable extends AuditedTable { } export type Penis = Selectable; + export interface PenisTable extends AuditedTable { id: GeneratedAlways; @@ -104,6 +113,7 @@ export interface PenisTable extends AuditedTable { } export type Boob = Selectable; + export interface BoobTable extends AuditedTable { id: GeneratedAlways; @@ -113,6 +123,7 @@ export interface BoobTable extends AuditedTable { } export type AustrianTranslation = Selectable; + export interface AustrianTranslationTable extends AuditedTable { id: GeneratedAlways; @@ -123,6 +134,7 @@ export interface AustrianTranslationTable extends AuditedTable { } export type EhreVotes = Selectable; + export interface EhreVotesTable extends AuditedTable { id: GeneratedAlways; @@ -130,6 +142,7 @@ export interface EhreVotesTable extends AuditedTable { } export type EhrePoints = Selectable; + export interface EhrePointsTable extends AuditedTable { id: GeneratedAlways; @@ -138,6 +151,7 @@ export interface EhrePointsTable extends AuditedTable { } export type FadingMessage = Selectable; + export interface FadingMessageTable extends AuditedTable { id: GeneratedAlways; @@ -150,6 +164,7 @@ export interface FadingMessageTable extends AuditedTable { } export type WoisAction = Selectable; + export interface WoisActionTable extends AuditedTable { id: GeneratedAlways; @@ -165,6 +180,7 @@ export interface WoisActionTable extends AuditedTable { export type DataUsage = "DELAYED_POLL"; export type AdditionalMessageData = Selectable; + export interface AdditionalMessageDataTable extends AuditedTable { id: GeneratedAlways; @@ -178,6 +194,7 @@ export interface AdditionalMessageDataTable extends AuditedTable { } export type Ban = Selectable; + export interface BanTable extends AuditedTable { id: GeneratedAlways; @@ -189,6 +206,7 @@ export interface BanTable extends AuditedTable { } export type Reminder = Selectable; + export interface ReminderTable extends AuditedTable { id: GeneratedAlways; @@ -207,6 +225,7 @@ export type LootOrigin = "drop" | "owner-transfer" | "replacement"; export type Loot = Selectable; export type LootInsertable = Insertable; + export interface LootTable extends AuditedTable { id: GeneratedAlways; @@ -231,6 +250,7 @@ export type LootAttributeId = number; export type LootAttribute = Selectable; export type LootAttributeInsertable = Insertable; + export interface LootAttributeTable extends AuditedTable { id: GeneratedAlways; @@ -246,6 +266,7 @@ export interface LootAttributeTable extends AuditedTable { } export type Emote = Selectable; + export interface EmoteTable extends AuditedTable { id: GeneratedAlways; @@ -259,6 +280,7 @@ export interface EmoteTable extends AuditedTable { } export type EmoteUse = Selectable; + export interface EmoteUseTable extends AuditedTable { id: GeneratedAlways; @@ -267,3 +289,17 @@ export interface EmoteUseTable extends AuditedTable { emoteId: ColumnType; isReaction: boolean; } + +interface FightHistoryTable extends AuditedTable { + id: GeneratedAlways; + userid: Snowflake; + bossname: string; + firsttime: boolean; +} + +interface FightInventoryTable { + id: GeneratedAlways; + userid: Snowflake; + lootId: LootId; + equippedSlot: string; +} diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts index 06b33848..3f8bc91b 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightinventory.ts @@ -1,14 +1,74 @@ -import type {User} from "discord.js"; +import type { User } from "discord.js"; import db from "@db"; +import { FightItemType } from "@/service/fightData.js"; +import type { LootId } from "@/storage/db/model.js"; +import * as lootDataService from "@/service/lootData.js"; +import * as lootService from "@/service/loot.js"; +export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { + return await ctx + .selectFrom("fightinventory") + .where("userid", "=", userId) + .selectAll() + .execute(); +} + +export async function getFightInventoryEnriched(userId: User["id"], ctx = db()) { + const unsorted = await getFightInventoryUnsorted(userId, ctx); + const enriched = []; + for (const equip of unsorted) { + enriched.push({ + item: await lootService.getUserLootById(userId, equip.lootId, ctx), + ...equip, + }); + } + return { + weapon: enriched.filter(value => value.equippedSlot === "weapon").shift(), + armor: enriched.filter(value => value.equippedSlot === "armor").shift(), + items: enriched.filter(value => value.equippedSlot === "item"), + }; +} -export async function getFightInventory(userId: User["id"], lootKindId: number, ctx = db()) { +export async function getItemsByType(userId: User["id"], fightItemType: string, ctx = db()) { return await ctx .selectFrom("fightinventory") .where("userid", "=", userId) + .where("equippedSlot", "=", fightItemType) .selectAll() .execute(); } -export async function +export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = db()) { + const item = await lootService.getUserLootById(userId, lootId); + const lootTemplate = lootDataService.resolveLootTemplate(item!.lootKindId)!; + const type = lootTemplate.gameEquip!.type; + const maxItems = { + weapon: 1, + armor: 1, + item: 3, + }; + const unequippeditems: string[] = []; + return await ctx.transaction().execute(async ctx => { + const equippedStuff = await getItemsByType(userId, type, ctx); + for (let i = 0; i <= equippedStuff.length - maxItems[type]; i++) { + const unequipitem = await lootService.getUserLootById( + userId, + equippedStuff[i].lootId, + ctx, + ); + unequippeditems.push(unequipitem?.displayName ?? String(equippedStuff[i].lootId)); + await ctx.deleteFrom("fightinventory").where("id", "=", equippedStuff[i].id).execute(); + } + + await ctx + .insertInto("fightinventory") + .values({ + userid: userId, + lootId: lootId, + equippedSlot: type, + }) + .execute(); + return { unequipped: unequippeditems, equipped: item }; + }); +} diff --git a/src/storage/loot.ts b/src/storage/loot.ts index 2d254e26..ca4785d3 100644 --- a/src/storage/loot.ts +++ b/src/storage/loot.ts @@ -21,7 +21,7 @@ import type { import db from "@db"; import type { LootAttributeKindId } from "@/service/lootData.js"; -import { Equipable } from "@/service/fightData.js"; +import type { Equipable } from "@/service/fightData.js"; export type LootUseCommandInteraction = ChatInputCommandInteraction & { channel: GuildTextBasedChannel; diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts index fe7d88fa..6e02f6fb 100644 --- a/src/storage/migrations/11-fightsystem.ts +++ b/src/storage/migrations/11-fightsystem.ts @@ -4,18 +4,15 @@ import { createUpdatedAtTrigger } from "@/storage/migrations/10-loot-attributes. export async function up(db: Kysely) { await db.schema .createTable("fightinventory") + .ifNotExists() .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) - .addColumn("gameType", "text", c => c.notNull()) - .addColumn("gameTemplateName", "text", c => c.notNull()) .addColumn("userid", "text") - .addColumn("lootId", "integer", c => c.references("loot.id").onDelete("cascade")) - .addColumn("equipped", "boolean", c => c.notNull()) - .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) - .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("lootId", "integer", c => c.references("loot.id")) + .addColumn("equippedSlot", "text") .execute(); - await createUpdatedAtTrigger(db, "fightinventory"); await db.schema .createTable("fighthistory") + .ifNotExists() .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) .addColumn("userid", "text", c => c.notNull()) .addColumn("bossName", "text", c => c.notNull()) From ff39e52193b5bf6bd7535e39c19622d397711488 Mon Sep 17 00:00:00 2001 From: Feluin Date: Sun, 15 Dec 2024 12:50:49 +0100 Subject: [PATCH 11/26] added fight with current inventory --- src/commands/fight.ts | 61 ++++++++++++++----------- src/commands/inventar.ts | 86 +++++++++++++++++------------------ src/service/fightData.ts | 59 ++++++++++++------------ src/storage/fightinventory.ts | 35 +++++++++----- 4 files changed, 129 insertions(+), 112 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 0079e321..d7d87795 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,4 +1,4 @@ -import type { ApplicationCommand } from "@/commands/command.js"; +import type {ApplicationCommand} from "@/commands/command.js"; import { type APIEmbed, APIEmbedField, @@ -8,12 +8,27 @@ import { ContextMenuCommandBuilder, type InteractionResponse, SlashCommandBuilder, - SlashCommandUserOption, + SlashCommandUserOption, User } from "discord.js"; -import type { BotContext } from "@/context.js"; -import type { JSONEncodable } from "@discordjs/util"; -import { type BaseEntity, bossMap, Entity, type FightScene } from "@/service/fightData.js"; -import { setTimeout } from "node:timers/promises"; +import type {BotContext} from "@/context.js"; +import type {JSONEncodable} from "@discordjs/util"; +import {type BaseEntity, baseStats, bossMap, Entity, type FightScene} from "@/service/fightData.js"; +import {setTimeout} from "node:timers/promises"; +import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; +import {resolveLootTemplate} from "@/service/lootData.js"; + +async function getFighter(user: User): Promise { + const userInventory = await getFightInventoryEnriched(user.id); + + + return { + ...baseStats, + name: user.displayName, + weapon: userInventory.weapon, + armor: userInventory.armor, + items: userInventory.items + }; +} export default class FightCommand implements ApplicationCommand { readonly description = "TBD"; @@ -31,24 +46,19 @@ export default class FightCommand implements ApplicationCommand { Object.entries(bossMap).map(boss => { return { name: boss[1].name, - value: boss[0], + value: boss[0] }; - }), - ), + }) + ) ); async handleInteraction(command: CommandInteraction, context: BotContext) { const boss = command.options.get("boss", true).value as string; const interactionResponse = await command.deferReply(); - const playerstats = { - name: "Player", - description: "", - health: 80, - baseDamage: 1, - baseDefence: 0, - items: [], - }; + console.log(boss); + const playerstats = await getFighter(command.user); + await fight(playerstats, bossMap[boss], interactionResponse); } } @@ -67,14 +77,14 @@ function checkWin(fightscene: FightScene): result { export async function fight( playerstats: BaseEntity, enemystats: BaseEntity, - interactionResponse: InteractionResponse>, + interactionResponse: InteractionResponse> ) { const enemy = new Entity(enemystats); const player = new Entity(playerstats); const scene: FightScene = { player: player, - enemy: enemy, + enemy: enemy }; while (checkWin(scene) === undefined) { player.itemtext = []; @@ -84,7 +94,6 @@ export async function fight( // then enemny hit enemy.attack(player); //special effects from items - for (const value of player.stats.items) { if (!value.afterFight) { continue; @@ -95,9 +104,9 @@ export async function fight( if (!value.afterFight) { continue; } - value.afterFight({ player: enemy, enemy: player }); + value.afterFight({player: enemy, enemy: player}); } - await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); + await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); await setTimeout(200); } } @@ -116,7 +125,7 @@ function renderStats(player: Entity) { 📚Items: ${player.itemtext.join("\n")} `, - inline: true, + inline: true }; } @@ -129,8 +138,8 @@ function renderFightEmbedded(fightscene: FightScene): JSONEncodable | renderStats(fightscene.enemy), { name: "Verlauf", - value: " ", - }, - ], + value: " " + } + ] }; } diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index f46cd1d8..867b2605 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -8,19 +8,19 @@ import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandUserOption, - type User, + type User } from "discord.js"; -import type { BotContext } from "@/context.js"; -import type { ApplicationCommand } from "@/commands/command.js"; +import type {BotContext} from "@/context.js"; +import type {ApplicationCommand} from "@/commands/command.js"; import * as lootService from "@/service/loot.js"; -import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; -import { format } from "@/utils/stringUtils.js"; +import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; +import {format} from "@/utils/stringUtils.js"; import * as lootDataService from "@/service/lootData.js"; -import { LootAttributeKindId } from "@/service/lootData.js"; +import {LootAttributeKindId} from "@/service/lootData.js"; import log from "@log"; -import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; +import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; export default class InventarCommand implements ApplicationCommand { name = "inventar"; @@ -33,7 +33,7 @@ export default class InventarCommand implements ApplicationCommand { new SlashCommandUserOption() .setRequired(false) .setName("user") - .setDescription("Wem du tun willst"), + .setDescription("Wem du tun willst") ) .addStringOption( new SlashCommandStringOption() @@ -41,10 +41,10 @@ export default class InventarCommand implements ApplicationCommand { .setDescription("Anzeige") .setRequired(false) .addChoices( - { name: "Kampfausrüstung", value: "fightinventory" }, - { name: "Kurzfassung", value: "short" }, - { name: "Detailansicht", value: "long" }, - ), + {name: "Kampfausrüstung", value: "fightinventory"}, + {name: "Kurzfassung", value: "short"}, + {name: "Detailansicht", value: "long"} + ) ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -56,7 +56,7 @@ export default class InventarCommand implements ApplicationCommand { const contents = await lootService.getInventoryContents(user); if (contents.length === 0) { await interaction.reply({ - content: "Dein Inventar ist ✨leer✨", + content: "Dein Inventar ist ✨leer✨" }); return; } @@ -75,7 +75,7 @@ export default class InventarCommand implements ApplicationCommand { async #createShortEmbed( context: BotContext, interaction: CommandInteraction, - user: User, + user: User ) { const contents = await lootService.getInventoryContents(user); const groupedByLoot = Object.groupBy(contents, item => item.displayName); @@ -98,7 +98,7 @@ export default class InventarCommand implements ApplicationCommand { .join("\n"); const cuties = contents.filter(i => - lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET), + lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET) ).length; const message = /* mf2 */ ` @@ -119,10 +119,10 @@ export default class InventarCommand implements ApplicationCommand { title: `Inventar von ${user.displayName}`, description, footer: { - text: format(message, { cuties, count: contents.length - cuties }), - }, - }, - ], + text: format(message, {cuties, count: contents.length - cuties}) + } + } + ] }); } @@ -131,7 +131,7 @@ export default class InventarCommand implements ApplicationCommand { const contentsUnsorted = await lootService.getInventoryContents(user); const contents = contentsUnsorted.toSorted((a, b) => - b.createdAt.localeCompare(a.createdAt), + b.createdAt.localeCompare(a.createdAt) ); let lastPageIndex = Math.floor(contents.length / pageSize); @@ -156,12 +156,12 @@ export default class InventarCommand implements ApplicationCommand { return { name: `${lootDataService.getEmote(context.guild, item)} ${item.displayName}${rarity} ${shortAttributeList}`.trim(), value: "", - inline: false, + inline: false }; }), footer: { - text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}`, - }, + text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}` + } } satisfies APIEmbed; return { @@ -174,19 +174,19 @@ export default class InventarCommand implements ApplicationCommand { label: "<<", customId: "page-prev", disabled: pageIndex <= 0, - style: ButtonStyle.Secondary, + style: ButtonStyle.Secondary }, { type: ComponentType.Button, label: ">>", customId: "page-next", disabled: pageIndex >= lastPageIndex, - style: ButtonStyle.Secondary, - }, - ], - }, + style: ButtonStyle.Secondary + } + ] + } ], - embeds: [embed], + embeds: [embed] } as const; } @@ -195,12 +195,12 @@ export default class InventarCommand implements ApplicationCommand { const message = await interaction.reply({ ...buildMessageData(pageIndex), fetchReply: true, - tts: false, + tts: false }); const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 45_000, + time: 45_000 }); collector.on("collect", async i => { @@ -218,13 +218,13 @@ export default class InventarCommand implements ApplicationCommand { } await message.edit({ - ...buildMessageData(pageIndex), + ...buildMessageData(pageIndex) }); }); - collector.on("end", async () => { + collector.on("end", async() => { await message.edit({ - components: [], + components: [] }); }); } @@ -235,29 +235,29 @@ export default class InventarCommand implements ApplicationCommand { title: `Kampfausrüstung von ${user.displayName}`, description: "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", - thumbnail: user.avatarURL() ? { url: user.avatarURL()! } : undefined, + thumbnail: user.avatarURL() ? {url: user.avatarURL()!} : undefined, fields: [ - { name: "Waffe", value: fightinventory.weapon?.item?.displayName ?? "Nix" }, - { name: "Rüstung", value: fightinventory.armor?.item?.displayName ?? "Nackt" }, + {name: "Waffe", value: fightinventory.weapon?.name ?? "Nix"}, + {name: "Rüstung", value: fightinventory.armor?.name ?? "Nackt"}, ...fightinventory.items.map(item => { return { - name: item.item!.displayName, + name: item.name, value: "Hier sollten die buffs stehen", - inline: true, + inline: true }; }), - { name: "Buffs", value: "Nix" }, + {name: "Buffs", value: "Nix"} ], footer: { - text: `Lol ist noch nicht fertig`, - }, + text: `Lol ist noch nicht fertig` + } } satisfies APIEmbed; const embed = { components: [], embeds: [display], fetchReply: true, - tts: false, + tts: false } as const satisfies InteractionReplyOptions; const message = await interaction.reply(embed); diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 2a9ef5ca..5eecbe4d 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -2,35 +2,35 @@ export const gameWeapons: { [name: string]: EquipableWeapon } = { dildo: { type: "weapon", name: "Dildo", - attack: { min: 3, max: 9 }, - }, + attack: {min: 3, max: 9} + } }; export const gameArmor: { [name: string]: EquipableArmor } = { nachthemd: { type: "armor", name: "Nachthemd", health: 50, - defence: { min: 2, max: 5 }, + defence: {min: 2, max: 5} }, eierwaermer: { type: "armor", name: "Eierwaermer", health: 30, - defence: { min: 3, max: 5 }, - }, + defence: {min: 3, max: 5} + } }; export const gameItems: { [name: string]: EquipableItem } = { ayran: { type: "item", name: "Ayran", - attackModifier: { min: 2, max: 3 }, + attackModifier: {min: 2, max: 3} }, oettinger: { type: "item", name: "Ötti", - attackModifier: { min: 1, max: 5 }, - defenceModifier: { min: -3, max: 0 }, - }, + attackModifier: {min: 1, max: 5}, + defenceModifier: {min: -3, max: 0} + } }; export const bossMap: { [name: string]: BaseEntity } = { gudrun: { @@ -41,7 +41,7 @@ export const bossMap: { [name: string]: BaseEntity } = { baseDefence: 0, armor: gameArmor.nachthemd, weapon: gameWeapons.dildo, - items: [], + items: [] }, deinchef: { @@ -50,7 +50,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [], + items: [] }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -58,7 +58,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [], + items: [] }, rentner: { name: "Reeeeeeentner", @@ -66,7 +66,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 200, baseDamage: 3, baseDefence: 5, - items: [], + items: [] }, barkeeper: { name: "Barkeeper aus Nürnia", @@ -76,21 +76,28 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 350, baseDamage: 5, baseDefence: 5, - items: [], - }, + items: [] + } +}; + +export const baseStats = { + description: "", + health: 80, + baseDamage: 1, + baseDefence: 0 }; export type FightItemType = "weapon" | "armor" | "item"; export type Equipable = EquipableWeapon | EquipableItem | EquipableArmor; -interface EquipableWeapon { +export interface EquipableWeapon { type: "weapon"; attack: Range; name: string; } -interface EquipableArmor { +export interface EquipableArmor { type: "armor"; defence: Range; health: number; @@ -142,17 +149,7 @@ export class Entity { const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( - this.stats.name + - " (" + - this.stats.health + - ") hits " + - enemy.stats.name + - " (" + - enemy.stats.health + - ") for " + - result.damage + - " mitigated " + - result.mitigated, + `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}` ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -179,7 +176,7 @@ export interface Range { type PermaBuff = {}; -interface EquipableItem { +export interface EquipableItem { type: "item"; name: string; attackModifier?: Range; @@ -194,7 +191,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; + return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; } - return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; + return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; } diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts index 3f8bc91b..c0925d4d 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightinventory.ts @@ -1,9 +1,10 @@ -import type { User } from "discord.js"; +import type {User} from "discord.js"; import db from "@db"; -import { FightItemType } from "@/service/fightData.js"; -import type { LootId } from "@/storage/db/model.js"; +import {Equipable, EquipableArmor, EquipableItem, EquipableWeapon, FightItemType} from "@/service/fightData.js"; +import type {LootId} from "@/storage/db/model.js"; import * as lootDataService from "@/service/lootData.js"; import * as lootService from "@/service/loot.js"; +import {LootKindId, resolveLootTemplate} from "@/service/lootData.js"; export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { return await ctx @@ -18,17 +19,27 @@ export async function getFightInventoryEnriched(userId: User["id"], ctx = db()) const enriched = []; for (const equip of unsorted) { enriched.push({ - item: await lootService.getUserLootById(userId, equip.lootId, ctx), - ...equip, + gameTemplate: await getGameTemplate((await lootService.getUserLootById(userId, equip.lootId, ctx))?.lootKindId), + ...equip }); } return { - weapon: enriched.filter(value => value.equippedSlot === "weapon").shift(), - armor: enriched.filter(value => value.equippedSlot === "armor").shift(), - items: enriched.filter(value => value.equippedSlot === "item"), + weapon: enriched.filter(value => value.gameTemplate?.type === "weapon") + .map(value => + value.gameTemplate as EquipableWeapon).shift(), + armor: enriched.filter(value => value.gameTemplate?.type === "armor") + .map(value => + value.gameTemplate as EquipableArmor).shift(), + items: enriched.filter(value => value.gameTemplate?.type === "item") + .map(value => + value.gameTemplate as EquipableItem) }; } +export async function getGameTemplate(lootKindId: LootKindId | undefined): Promise { + return lootKindId ? resolveLootTemplate(lootKindId)?.gameEquip : undefined; +} + export async function getItemsByType(userId: User["id"], fightItemType: string, ctx = db()) { return await ctx .selectFrom("fightinventory") @@ -45,7 +56,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const maxItems = { weapon: 1, armor: 1, - item: 3, + item: 3 }; const unequippeditems: string[] = []; @@ -55,7 +66,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const unequipitem = await lootService.getUserLootById( userId, equippedStuff[i].lootId, - ctx, + ctx ); unequippeditems.push(unequipitem?.displayName ?? String(equippedStuff[i].lootId)); await ctx.deleteFrom("fightinventory").where("id", "=", equippedStuff[i].id).execute(); @@ -66,9 +77,9 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = .values({ userid: userId, lootId: lootId, - equippedSlot: type, + equippedSlot: type }) .execute(); - return { unequipped: unequippeditems, equipped: item }; + return {unequipped: unequippeditems, equipped: item}; }); } From 7423034af88ca8c2f507a87a49dfa86806a8be75 Mon Sep 17 00:00:00 2001 From: Feluin Date: Sun, 15 Dec 2024 12:51:11 +0100 Subject: [PATCH 12/26] added fight with current inventory --- src/commands/fight.ts | 48 +++++++++++--------- src/commands/inventar.ts | 84 +++++++++++++++++------------------ src/service/fightData.ts | 38 ++++++++-------- src/storage/fightinventory.ts | 52 +++++++++++++--------- 4 files changed, 120 insertions(+), 102 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index d7d87795..c1db8174 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,4 +1,4 @@ -import type {ApplicationCommand} from "@/commands/command.js"; +import type { ApplicationCommand } from "@/commands/command.js"; import { type APIEmbed, APIEmbedField, @@ -8,25 +8,31 @@ import { ContextMenuCommandBuilder, type InteractionResponse, SlashCommandBuilder, - SlashCommandUserOption, User + SlashCommandUserOption, + User, } from "discord.js"; -import type {BotContext} from "@/context.js"; -import type {JSONEncodable} from "@discordjs/util"; -import {type BaseEntity, baseStats, bossMap, Entity, type FightScene} from "@/service/fightData.js"; -import {setTimeout} from "node:timers/promises"; -import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; -import {resolveLootTemplate} from "@/service/lootData.js"; +import type { BotContext } from "@/context.js"; +import type { JSONEncodable } from "@discordjs/util"; +import { + type BaseEntity, + baseStats, + bossMap, + Entity, + type FightScene, +} from "@/service/fightData.js"; +import { setTimeout } from "node:timers/promises"; +import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; +import { resolveLootTemplate } from "@/service/lootData.js"; async function getFighter(user: User): Promise { const userInventory = await getFightInventoryEnriched(user.id); - return { ...baseStats, name: user.displayName, weapon: userInventory.weapon, armor: userInventory.armor, - items: userInventory.items + items: userInventory.items, }; } @@ -46,10 +52,10 @@ export default class FightCommand implements ApplicationCommand { Object.entries(bossMap).map(boss => { return { name: boss[1].name, - value: boss[0] + value: boss[0], }; - }) - ) + }), + ), ); async handleInteraction(command: CommandInteraction, context: BotContext) { @@ -77,14 +83,14 @@ function checkWin(fightscene: FightScene): result { export async function fight( playerstats: BaseEntity, enemystats: BaseEntity, - interactionResponse: InteractionResponse> + interactionResponse: InteractionResponse>, ) { const enemy = new Entity(enemystats); const player = new Entity(playerstats); const scene: FightScene = { player: player, - enemy: enemy + enemy: enemy, }; while (checkWin(scene) === undefined) { player.itemtext = []; @@ -104,9 +110,9 @@ export async function fight( if (!value.afterFight) { continue; } - value.afterFight({player: enemy, enemy: player}); + value.afterFight({ player: enemy, enemy: player }); } - await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); + await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); await setTimeout(200); } } @@ -125,7 +131,7 @@ function renderStats(player: Entity) { 📚Items: ${player.itemtext.join("\n")} `, - inline: true + inline: true, }; } @@ -138,8 +144,8 @@ function renderFightEmbedded(fightscene: FightScene): JSONEncodable | renderStats(fightscene.enemy), { name: "Verlauf", - value: " " - } - ] + value: " ", + }, + ], }; } diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index 867b2605..7c1b921c 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -8,19 +8,19 @@ import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandUserOption, - type User + type User, } from "discord.js"; -import type {BotContext} from "@/context.js"; -import type {ApplicationCommand} from "@/commands/command.js"; +import type { BotContext } from "@/context.js"; +import type { ApplicationCommand } from "@/commands/command.js"; import * as lootService from "@/service/loot.js"; -import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; -import {format} from "@/utils/stringUtils.js"; +import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; +import { format } from "@/utils/stringUtils.js"; import * as lootDataService from "@/service/lootData.js"; -import {LootAttributeKindId} from "@/service/lootData.js"; +import { LootAttributeKindId } from "@/service/lootData.js"; import log from "@log"; -import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; +import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; export default class InventarCommand implements ApplicationCommand { name = "inventar"; @@ -33,7 +33,7 @@ export default class InventarCommand implements ApplicationCommand { new SlashCommandUserOption() .setRequired(false) .setName("user") - .setDescription("Wem du tun willst") + .setDescription("Wem du tun willst"), ) .addStringOption( new SlashCommandStringOption() @@ -41,10 +41,10 @@ export default class InventarCommand implements ApplicationCommand { .setDescription("Anzeige") .setRequired(false) .addChoices( - {name: "Kampfausrüstung", value: "fightinventory"}, - {name: "Kurzfassung", value: "short"}, - {name: "Detailansicht", value: "long"} - ) + { name: "Kampfausrüstung", value: "fightinventory" }, + { name: "Kurzfassung", value: "short" }, + { name: "Detailansicht", value: "long" }, + ), ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -56,7 +56,7 @@ export default class InventarCommand implements ApplicationCommand { const contents = await lootService.getInventoryContents(user); if (contents.length === 0) { await interaction.reply({ - content: "Dein Inventar ist ✨leer✨" + content: "Dein Inventar ist ✨leer✨", }); return; } @@ -75,7 +75,7 @@ export default class InventarCommand implements ApplicationCommand { async #createShortEmbed( context: BotContext, interaction: CommandInteraction, - user: User + user: User, ) { const contents = await lootService.getInventoryContents(user); const groupedByLoot = Object.groupBy(contents, item => item.displayName); @@ -98,7 +98,7 @@ export default class InventarCommand implements ApplicationCommand { .join("\n"); const cuties = contents.filter(i => - lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET) + lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET), ).length; const message = /* mf2 */ ` @@ -119,10 +119,10 @@ export default class InventarCommand implements ApplicationCommand { title: `Inventar von ${user.displayName}`, description, footer: { - text: format(message, {cuties, count: contents.length - cuties}) - } - } - ] + text: format(message, { cuties, count: contents.length - cuties }), + }, + }, + ], }); } @@ -131,7 +131,7 @@ export default class InventarCommand implements ApplicationCommand { const contentsUnsorted = await lootService.getInventoryContents(user); const contents = contentsUnsorted.toSorted((a, b) => - b.createdAt.localeCompare(a.createdAt) + b.createdAt.localeCompare(a.createdAt), ); let lastPageIndex = Math.floor(contents.length / pageSize); @@ -156,12 +156,12 @@ export default class InventarCommand implements ApplicationCommand { return { name: `${lootDataService.getEmote(context.guild, item)} ${item.displayName}${rarity} ${shortAttributeList}`.trim(), value: "", - inline: false + inline: false, }; }), footer: { - text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}` - } + text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}`, + }, } satisfies APIEmbed; return { @@ -174,19 +174,19 @@ export default class InventarCommand implements ApplicationCommand { label: "<<", customId: "page-prev", disabled: pageIndex <= 0, - style: ButtonStyle.Secondary + style: ButtonStyle.Secondary, }, { type: ComponentType.Button, label: ">>", customId: "page-next", disabled: pageIndex >= lastPageIndex, - style: ButtonStyle.Secondary - } - ] - } + style: ButtonStyle.Secondary, + }, + ], + }, ], - embeds: [embed] + embeds: [embed], } as const; } @@ -195,12 +195,12 @@ export default class InventarCommand implements ApplicationCommand { const message = await interaction.reply({ ...buildMessageData(pageIndex), fetchReply: true, - tts: false + tts: false, }); const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 45_000 + time: 45_000, }); collector.on("collect", async i => { @@ -218,13 +218,13 @@ export default class InventarCommand implements ApplicationCommand { } await message.edit({ - ...buildMessageData(pageIndex) + ...buildMessageData(pageIndex), }); }); - collector.on("end", async() => { + collector.on("end", async () => { await message.edit({ - components: [] + components: [], }); }); } @@ -235,29 +235,29 @@ export default class InventarCommand implements ApplicationCommand { title: `Kampfausrüstung von ${user.displayName}`, description: "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", - thumbnail: user.avatarURL() ? {url: user.avatarURL()!} : undefined, + thumbnail: user.avatarURL() ? { url: user.avatarURL()! } : undefined, fields: [ - {name: "Waffe", value: fightinventory.weapon?.name ?? "Nix"}, - {name: "Rüstung", value: fightinventory.armor?.name ?? "Nackt"}, + { name: "Waffe", value: fightinventory.weapon?.name ?? "Nix" }, + { name: "Rüstung", value: fightinventory.armor?.name ?? "Nackt" }, ...fightinventory.items.map(item => { return { name: item.name, value: "Hier sollten die buffs stehen", - inline: true + inline: true, }; }), - {name: "Buffs", value: "Nix"} + { name: "Buffs", value: "Nix" }, ], footer: { - text: `Lol ist noch nicht fertig` - } + text: `Lol ist noch nicht fertig`, + }, } satisfies APIEmbed; const embed = { components: [], embeds: [display], fetchReply: true, - tts: false + tts: false, } as const satisfies InteractionReplyOptions; const message = await interaction.reply(embed); diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 5eecbe4d..dbbbbc25 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -2,35 +2,35 @@ export const gameWeapons: { [name: string]: EquipableWeapon } = { dildo: { type: "weapon", name: "Dildo", - attack: {min: 3, max: 9} - } + attack: { min: 3, max: 9 }, + }, }; export const gameArmor: { [name: string]: EquipableArmor } = { nachthemd: { type: "armor", name: "Nachthemd", health: 50, - defence: {min: 2, max: 5} + defence: { min: 2, max: 5 }, }, eierwaermer: { type: "armor", name: "Eierwaermer", health: 30, - defence: {min: 3, max: 5} - } + defence: { min: 3, max: 5 }, + }, }; export const gameItems: { [name: string]: EquipableItem } = { ayran: { type: "item", name: "Ayran", - attackModifier: {min: 2, max: 3} + attackModifier: { min: 2, max: 3 }, }, oettinger: { type: "item", name: "Ötti", - attackModifier: {min: 1, max: 5}, - defenceModifier: {min: -3, max: 0} - } + attackModifier: { min: 1, max: 5 }, + defenceModifier: { min: -3, max: 0 }, + }, }; export const bossMap: { [name: string]: BaseEntity } = { gudrun: { @@ -41,7 +41,7 @@ export const bossMap: { [name: string]: BaseEntity } = { baseDefence: 0, armor: gameArmor.nachthemd, weapon: gameWeapons.dildo, - items: [] + items: [], }, deinchef: { @@ -50,7 +50,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [] + items: [], }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -58,7 +58,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [] + items: [], }, rentner: { name: "Reeeeeeentner", @@ -66,7 +66,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 200, baseDamage: 3, baseDefence: 5, - items: [] + items: [], }, barkeeper: { name: "Barkeeper aus Nürnia", @@ -76,15 +76,15 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 350, baseDamage: 5, baseDefence: 5, - items: [] - } + items: [], + }, }; export const baseStats = { description: "", health: 80, baseDamage: 1, - baseDefence: 0 + baseDefence: 0, }; export type FightItemType = "weapon" | "armor" | "item"; @@ -149,7 +149,7 @@ export class Entity { const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( - `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}` + `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -191,7 +191,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; + return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; } - return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; + return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; } diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts index c0925d4d..ef6dd061 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightinventory.ts @@ -1,10 +1,16 @@ -import type {User} from "discord.js"; +import type { User } from "discord.js"; import db from "@db"; -import {Equipable, EquipableArmor, EquipableItem, EquipableWeapon, FightItemType} from "@/service/fightData.js"; -import type {LootId} from "@/storage/db/model.js"; +import { + Equipable, + EquipableArmor, + EquipableItem, + EquipableWeapon, + FightItemType, +} from "@/service/fightData.js"; +import type { LootId } from "@/storage/db/model.js"; import * as lootDataService from "@/service/lootData.js"; import * as lootService from "@/service/loot.js"; -import {LootKindId, resolveLootTemplate} from "@/service/lootData.js"; +import { LootKindId, resolveLootTemplate } from "@/service/lootData.js"; export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { return await ctx @@ -19,24 +25,30 @@ export async function getFightInventoryEnriched(userId: User["id"], ctx = db()) const enriched = []; for (const equip of unsorted) { enriched.push({ - gameTemplate: await getGameTemplate((await lootService.getUserLootById(userId, equip.lootId, ctx))?.lootKindId), - ...equip + gameTemplate: await getGameTemplate( + (await lootService.getUserLootById(userId, equip.lootId, ctx))?.lootKindId, + ), + ...equip, }); } return { - weapon: enriched.filter(value => value.gameTemplate?.type === "weapon") - .map(value => - value.gameTemplate as EquipableWeapon).shift(), - armor: enriched.filter(value => value.gameTemplate?.type === "armor") - .map(value => - value.gameTemplate as EquipableArmor).shift(), - items: enriched.filter(value => value.gameTemplate?.type === "item") - .map(value => - value.gameTemplate as EquipableItem) + weapon: enriched + .filter(value => value.gameTemplate?.type === "weapon") + .map(value => value.gameTemplate as EquipableWeapon) + .shift(), + armor: enriched + .filter(value => value.gameTemplate?.type === "armor") + .map(value => value.gameTemplate as EquipableArmor) + .shift(), + items: enriched + .filter(value => value.gameTemplate?.type === "item") + .map(value => value.gameTemplate as EquipableItem), }; } -export async function getGameTemplate(lootKindId: LootKindId | undefined): Promise { +export async function getGameTemplate( + lootKindId: LootKindId | undefined, +): Promise { return lootKindId ? resolveLootTemplate(lootKindId)?.gameEquip : undefined; } @@ -56,7 +68,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const maxItems = { weapon: 1, armor: 1, - item: 3 + item: 3, }; const unequippeditems: string[] = []; @@ -66,7 +78,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const unequipitem = await lootService.getUserLootById( userId, equippedStuff[i].lootId, - ctx + ctx, ); unequippeditems.push(unequipitem?.displayName ?? String(equippedStuff[i].lootId)); await ctx.deleteFrom("fightinventory").where("id", "=", equippedStuff[i].id).execute(); @@ -77,9 +89,9 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = .values({ userid: userId, lootId: lootId, - equippedSlot: type + equippedSlot: type, }) .execute(); - return {unequipped: unequippeditems, equipped: item}; + return { unequipped: unequippeditems, equipped: item }; }); } From e62142d46659b2e3b436b5fbe0b58eb80d8c712a Mon Sep 17 00:00:00 2001 From: Feluin Date: Mon, 23 Dec 2024 19:28:19 +0100 Subject: [PATCH 13/26] fixed multiselection --- src/commands/fight.ts | 27 +++----- src/commands/gegenstand.ts | 129 +++++++++++++++++++------------------ src/service/fightData.ts | 74 +++++++++++---------- src/service/lootData.ts | 126 ++++++++++++++++++------------------ 4 files changed, 175 insertions(+), 181 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index c1db8174..371f0d0f 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,28 +1,19 @@ -import type { ApplicationCommand } from "@/commands/command.js"; +import type {ApplicationCommand} from "@/commands/command.js"; import { type APIEmbed, APIEmbedField, type BooleanCache, type CacheType, type CommandInteraction, - ContextMenuCommandBuilder, type InteractionResponse, SlashCommandBuilder, - SlashCommandUserOption, - User, + User } from "discord.js"; -import type { BotContext } from "@/context.js"; -import type { JSONEncodable } from "@discordjs/util"; -import { - type BaseEntity, - baseStats, - bossMap, - Entity, - type FightScene, -} from "@/service/fightData.js"; -import { setTimeout } from "node:timers/promises"; -import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; -import { resolveLootTemplate } from "@/service/lootData.js"; +import type {BotContext} from "@/context.js"; +import type {JSONEncodable} from "@discordjs/util"; +import {type BaseEntity, baseStats, bossMap, Entity, type FightScene} from "@/service/fightData.js"; +import {setTimeout} from "node:timers/promises"; +import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; async function getFighter(user: User): Promise { const userInventory = await getFightInventoryEnriched(user.id); @@ -126,8 +117,8 @@ function renderStats(player: Entity) { name: player.stats.name, value: `❤️HP${player.stats.health}/${player.maxhealth} ❤️${"=".repeat(Math.max(0, (player.stats.health / player.maxhealth) * 10))} - ⚔️Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastattack} - 🛡️Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastdefence} + ⚔️Waffe: ${player.stats.weapon ?? "Schwengel"} ${player.lastattack} + 🛡️Rüstung: ${player.stats.armor ?? "Nackt"} ${player.lastdefence} 📚Items: ${player.itemtext.join("\n")} `, diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index 61570f8c..46f647ba 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -7,24 +7,24 @@ import { type CommandInteraction, SlashCommandBuilder, SlashCommandStringOption, - SlashCommandSubcommandBuilder, + SlashCommandSubcommandBuilder } from "discord.js"; import * as sentry from "@sentry/bun"; -import type { BotContext } from "@/context.js"; -import type { ApplicationCommand } from "@/commands/command.js"; -import type { LootUseCommandInteraction } from "@/storage/loot.js"; +import type {BotContext} from "@/context.js"; +import type {ApplicationCommand} from "@/commands/command.js"; +import type {LootUseCommandInteraction} from "@/storage/loot.js"; import * as lootService from "@/service/loot.js"; import * as lootRoleService from "@/service/lootRoles.js"; -import { randomEntry } from "@/utils/arrayUtils.js"; -import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; +import {randomEntry} from "@/utils/arrayUtils.js"; +import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; import * as imageService from "@/service/image.js"; import * as lootDataService from "@/service/lootData.js"; -import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; +import {LootAttributeClassId, LootAttributeKindId, LootKindId} from "@/service/lootData.js"; import log from "@log"; -import { equipItembyLoot } from "@/storage/fightinventory.js"; +import {equipItembyLoot, getFightInventoryUnsorted} from "@/storage/fightinventory.js"; export default class GegenstandCommand implements ApplicationCommand { name = "gegenstand"; @@ -36,7 +36,7 @@ export default class GegenstandCommand implements ApplicationCommand { .addSubcommand( new SlashCommandSubcommandBuilder() .setName("entsorgen") - .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes"), + .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes") ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -47,8 +47,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Der Gegenstand, über den du Informationen haben möchtest") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -59,8 +59,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du benutzen möchtest") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -71,8 +71,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Rüste dich für deinen nächsten Kampf") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -104,33 +104,33 @@ export default class GegenstandCommand implements ApplicationCommand { { description: "Es ist kein Wärter im Dienst. Das Tor ist zu. Du rennst dagegen. Opfer.", - color: 0xff0000, - }, - ], + color: 0xff0000 + } + ] }); return; } const wasteContents = await lootService.getUserLootsByTypeId( interaction.user.id, - LootKindId.RADIOACTIVE_WASTE, + LootKindId.RADIOACTIVE_WASTE ); if (wasteContents.length === 0) { await interaction.reply({ - content: "Du hast keinen Atommüll, den du in die Grube werfen kannst.", + content: "Du hast keinen Atommüll, den du in die Grube werfen kannst." }); return; } const sweetContent = await lootService.getUserLootsWithAttribute( interaction.user.id, - LootAttributeKindId.SWEET, + LootAttributeKindId.SWEET ); if (sweetContent.length === 0) { await interaction.reply({ - content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst.", + content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst." }); return; } @@ -142,7 +142,7 @@ export default class GegenstandCommand implements ApplicationCommand { const messages = [ `Du hast dem Wärter ${currentGuard} etwas Atommüll und etwas Süßes zum Naschen gegeben.`, `${currentGuard} hat sich über deinen Atommüll und die süßen Sachen gefreut.`, - `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.`, + `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.` ]; await interaction.reply({ @@ -151,11 +151,11 @@ export default class GegenstandCommand implements ApplicationCommand { title: "Atommüll entsorgt!", description: randomEntry(messages), footer: { - text: "Jetzt ist es das Problem des deutschen Steuerzahlers", + text: "Jetzt ist es das Problem des deutschen Steuerzahlers" }, - color: 0x00ff00, - }, - ], + color: 0x00ff00 + } + ] }); } @@ -172,7 +172,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const { item, template, attributes } = info; + const {item, template, attributes} = info; const effects = template.effects ?? []; @@ -202,14 +202,14 @@ export default class GegenstandCommand implements ApplicationCommand { const extraFields: (APIEmbedField | undefined)[] = [ template.onUse !== undefined - ? { name: "🔧 Benutzbar", value: "", inline: true } + ? {name: "🔧 Benutzbar", value: "", inline: true} : undefined, ...otherAttributes.map(attribute => ({ name: `${attribute.shortDisplay} ${attribute.displayName}`.trim(), value: "", - inline: true, - })), + inline: true + })) ]; await interaction.reply({ @@ -220,31 +220,31 @@ export default class GegenstandCommand implements ApplicationCommand { color: 0x00ff00, image: attachment ? { - url: "attachment://hero.gif", - width: 128, - } + url: "attachment://hero.gif", + width: 128 + } : undefined, fields: [ ...effects.map(value => ({ name: "⚡️ Effekt", value, - inline: true, + inline: true })), - ...extraFields.filter(e => e !== undefined), + ...extraFields.filter(e => e !== undefined) ], footer: { - text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim(), - }, - }, + text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim() + } + } ], files: attachment ? [ - { - name: "hero.gif", - attachment, - }, - ] - : [], + { + name: "hero.gif", + attachment + } + ] + : [] }); } @@ -261,12 +261,12 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const { item, template } = info; + const {item, template} = info; if (template.onUse === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht benutzt werden.", - ephemeral: true, + ephemeral: true }); return; } @@ -276,14 +276,14 @@ export default class GegenstandCommand implements ApplicationCommand { keepInInventory = await template.onUse( interaction as LootUseCommandInteraction, context, - item, + item ); } catch (error) { log.error(error, "Error while using item"); sentry.captureException(error); await interaction.reply({ content: "Beim Benutzen dieses Gegenstands ist ein Fehler aufgetreten.", - ephemeral: true, + ephemeral: true }); } @@ -302,7 +302,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (!item) { await interaction.reply({ content: "Diesen Gegensand hast du nicht.", - ephemeral: true, + ephemeral: true }); return; } @@ -311,14 +311,14 @@ export default class GegenstandCommand implements ApplicationCommand { if (!template) { await interaction.reply({ content: "Dieser Gegenstand ist unbekannt.", - ephemeral: true, + ephemeral: true }); return; } const attributes = await lootService.getLootAttributes(item.id); - return { item, template, attributes }; + return {item, template, attributes}; } async autocomplete(interaction: AutocompleteInteraction) { @@ -362,7 +362,7 @@ export default class GegenstandCommand implements ApplicationCommand { typeof emote === "string" ? `${emote} ${item.displayName} (${item.id})` : `${item.displayName} (${item.id})`, - value: String(item.id), + value: String(item.id) }); } @@ -381,12 +381,20 @@ export default class GegenstandCommand implements ApplicationCommand { if (!info) { return; } - const { item, template } = info; + const {item, template} = info; log.info(item); if (template.gameEquip === undefined) { await interaction.reply({ - content: "Dieser Gegenstand kann nicht ausgerüstet werden.", - ephemeral: true, + content: ` ${item.displayName} kann nicht ausgerüstet werden.`, + ephemeral: true + }); + return; + } + const items = await getFightInventoryUnsorted(interaction.user.id); + if (items.filter(i => i.id == item.id)) { + await interaction.reply({ + content: `Du hast ${item.displayName} schon ausgerüstet`, + ephemeral: true }); return; } @@ -394,13 +402,8 @@ export default class GegenstandCommand implements ApplicationCommand { log.info(result); const message = result.unequipped.length == 0 - ? "Du hast " + result.equipped?.displayName + " ausgerüstet" - : "Du hast " + - result.unequipped.join(", ") + - " abgelegt und dafür " + - "" + - result.equipped?.displayName + - " ausgerüstet"; + ? `Du hast ${result.equipped?.displayName} ausgerüstet` + : `Du hast ${result.unequipped.join(", ")} abgelegt und dafür ${result.equipped?.displayName} ausgerüstet`; await interaction.reply(message); } } diff --git a/src/service/fightData.ts b/src/service/fightData.ts index dbbbbc25..9bbd345b 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,36 +1,37 @@ -export const gameWeapons: { [name: string]: EquipableWeapon } = { - dildo: { - type: "weapon", - name: "Dildo", - attack: { min: 3, max: 9 }, +export const fightTemplates: { [name: string]: Equipable } = { + ayran: { + type: "item", + attackModifier: {min: 2, max: 3} + }, + oettinger: { + type: "item", + attackModifier: {min: 1, max: 5}, + defenceModifier: {min: -3, max: 0} + }, + thunfischshake: { + type: "item", + attackModifier: {min: 3, max: 5} }, -}; -export const gameArmor: { [name: string]: EquipableArmor } = { nachthemd: { type: "armor", - name: "Nachthemd", + health: 50, - defence: { min: 2, max: 5 }, + defence: {min: 2, max: 5} }, eierwaermer: { type: "armor", - name: "Eierwaermer", + health: 30, - defence: { min: 3, max: 5 }, + defence: {min: 3, max: 5} }, -}; -export const gameItems: { [name: string]: EquipableItem } = { - ayran: { - type: "item", - name: "Ayran", - attackModifier: { min: 2, max: 3 }, - }, - oettinger: { - type: "item", - name: "Ötti", - attackModifier: { min: 1, max: 5 }, - defenceModifier: { min: -3, max: 0 }, + dildo: { + type: "weapon", + attack: {min: 3, max: 9} }, + messerblock: { + type: "weapon", + attack: {min: 1, max: 9} + } }; export const bossMap: { [name: string]: BaseEntity } = { gudrun: { @@ -39,9 +40,9 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 150, baseDamage: 2, baseDefence: 0, - armor: gameArmor.nachthemd, - weapon: gameWeapons.dildo, - items: [], + armor: fightTemplates.nachthemd as EquipableArmor, + weapon: fightTemplates.dildo as EquipableWeapon, + items: [] }, deinchef: { @@ -50,7 +51,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [], + items: [] }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -58,7 +59,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [], + items: [] }, rentner: { name: "Reeeeeeentner", @@ -66,7 +67,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 200, baseDamage: 3, baseDefence: 5, - items: [], + items: [] }, barkeeper: { name: "Barkeeper aus Nürnia", @@ -76,15 +77,15 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 350, baseDamage: 5, baseDefence: 5, - items: [], - }, + items: [] + } }; export const baseStats = { description: "", health: 80, baseDamage: 1, - baseDefence: 0, + baseDefence: 0 }; export type FightItemType = "weapon" | "armor" | "item"; @@ -94,14 +95,12 @@ export type Equipable = EquipableWeapon | EquipableItem | EquipableArmor; export interface EquipableWeapon { type: "weapon"; attack: Range; - name: string; } export interface EquipableArmor { type: "armor"; defence: Range; health: number; - name: string; } export interface FightScene { @@ -149,7 +148,7 @@ export class Entity { const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( - `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, + `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}` ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -178,7 +177,6 @@ type PermaBuff = {}; export interface EquipableItem { type: "item"; - name: string; attackModifier?: Range; defenceModifier?: Range; afterFight?: (scene: FightScene) => void; @@ -191,7 +189,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; + return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; } - return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; + return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; } diff --git a/src/service/lootData.ts b/src/service/lootData.ts index 837f00e4..a2656658 100644 --- a/src/service/lootData.ts +++ b/src/service/lootData.ts @@ -1,12 +1,12 @@ -import type { LootAttributeTemplate, LootTemplate } from "@/storage/loot.js"; +import type {LootAttributeTemplate, LootTemplate} from "@/storage/loot.js"; import * as lootDropService from "@/service/lootDrop.js"; import * as emoteService from "@/service/emote.js"; -import { GuildMember, type Guild } from "discord.js"; -import type { Loot, LootAttribute } from "@/storage/db/model.js"; +import {GuildMember, type Guild} from "discord.js"; +import type {Loot, LootAttribute} from "@/storage/db/model.js"; import log from "@log"; -import { gameItems } from "@/service/fightData.js"; +import {fightTemplates} from "@/service/fightData.js"; const ACHTUNG_NICHT_DROPBAR_WEIGHT_KG = 0; @@ -73,7 +73,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "✨Nichts✨", dropDescription: "¯\\_(ツ)_/¯", asset: null, - excludeFromInventory: true, + excludeFromInventory: true }, [LootKindId.KADSE]: { id: LootKindId.KADSE, @@ -84,8 +84,8 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: ":catsmile:", asset: "assets/loot/01-kadse.jpg", attributeAsset: { - [LootAttributeKindId.RADIOACTIVE]: "assets/loot/attributes/01-kadse-verstrahlt.jpg", - }, + [LootAttributeKindId.RADIOACTIVE]: "assets/loot/attributes/01-kadse-verstrahlt.jpg" + } }, [LootKindId.MESSERBLOCK]: { id: LootKindId.MESSERBLOCK, @@ -95,6 +95,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "🔪", emote: "🔪", asset: "assets/loot/02-messerblock.jpg", + gameEquip: fightTemplates.messerblock }, [LootKindId.KUEHLSCHRANK]: { id: LootKindId.KUEHLSCHRANK, @@ -105,7 +106,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { "Dafür haben wir keine Kosten und Mühen gescheut und extra einen Kredit aufgenommen.", emote: "🧊", asset: "assets/loot/03-kuehlschrank.jpg", - effects: ["Lässt Essen nicht schimmeln"], + effects: ["Lässt Essen nicht schimmeln"] }, [LootKindId.DOENER]: { id: LootKindId.DOENER, @@ -114,7 +115,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Döner", dropDescription: "Bewahre ihn gut als Geldanlage auf!", emote: "🥙", - asset: "assets/loot/04-doener.jpg", + asset: "assets/loot/04-doener.jpg" }, [LootKindId.KINN]: { id: LootKindId.KINN, @@ -123,7 +124,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Ein Kinn", dropDescription: "Pass gut drauf auf, sonst flieht es!", emote: "👶", - asset: "assets/loot/05-kinn.jpg", + asset: "assets/loot/05-kinn.jpg" }, [LootKindId.KRANKSCHREIBUNG]: { id: LootKindId.KRANKSCHREIBUNG, @@ -135,7 +136,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { "Mit der Krankschreibung kannst du deine Wärterschicht abbrechen und dich ausruhen.", emote: "🩺", asset: "assets/loot/06-krankschreibung.jpg", - onUse: async (interaction, context, loot) => { + onUse: async(interaction, context, loot) => { const lootRoles = await import("./lootRoles.js"); const member = interaction.member; if (!member || !(member instanceof GuildMember)) { @@ -145,18 +146,18 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { if (!isOnDuty) { await interaction.reply( - "Du bist gar nicht im Dienst, aber hast dir deinen Urlaub trotzdem wohl verdient.", + "Du bist gar nicht im Dienst, aber hast dir deinen Urlaub trotzdem wohl verdient." ); return false; } await lootRoles.endAsseGuardShift(context, member); await interaction.reply( - "Du hast kränkelnd beim Werksleiter angerufen und dich krankgemeldet. Genieße deinen Tag zu Hause!", + "Du hast kränkelnd beim Werksleiter angerufen und dich krankgemeldet. Genieße deinen Tag zu Hause!" ); return false; - }, + } }, [LootKindId.WUERFELWURF]: { id: LootKindId.WUERFELWURF, @@ -167,10 +168,10 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "🎲", asset: "assets/loot/07-wuerfelwurf.jpg", excludeFromInventory: true, - onDrop: async (_content, winner, channel, _loot) => { + onDrop: async(_content, winner, channel, _loot) => { const rollService = await import("./roll.js"); await rollService.rollInChannel(winner.user, channel, 1, 6); - }, + } }, [LootKindId.GESCHENK]: { id: LootKindId.GESCHENK, @@ -180,15 +181,15 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Du kannst jemand anderem eine Freude machen :feelsamazingman:", emote: "🎁", asset: null, - onUse: async (interaction, context, loot) => { + onUse: async(interaction, context, loot) => { await lootDropService.postLootDrop( context, interaction.channel, interaction.user, - loot.id, + loot.id ); return false; - }, + } }, [LootKindId.AYRAN]: { id: LootKindId.AYRAN, @@ -198,7 +199,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Der gute von Müller", emote: "🥛", asset: "assets/loot/09-ayran.jpg", - gameEquip: gameItems.ayran, + gameEquip: fightTemplates.ayran }, [LootKindId.PKV]: { id: LootKindId.PKV, @@ -208,7 +209,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Fehlt dir nur noch das Geld zum Vorstrecken", emote: "💉", asset: "assets/loot/10-pkv.jpg", - effects: ["` +100% ` Chance auf AU 🟢"], + effects: ["` +100% ` Chance auf AU 🟢"] }, [LootKindId.TRICHTER]: { id: LootKindId.TRICHTER, @@ -217,7 +218,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Trichter", dropDescription: "Für die ganz großen Schlücke", emote: ":trichter:", - asset: "assets/loot/11-trichter.jpg", + asset: "assets/loot/11-trichter.jpg" }, [LootKindId.GRAFIKKARTE]: { id: LootKindId.GRAFIKKARTE, @@ -226,7 +227,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine Grafikkarte aus der Zukunft", dropDescription: "Leider ohne Treiber, die gibt es erst in 3 Monaten", emote: "🖥️", - asset: "assets/loot/12-grafikkarte.png", + asset: "assets/loot/12-grafikkarte.png" }, [LootKindId.HAENDEDRUCK]: { id: LootKindId.HAENDEDRUCK, @@ -236,7 +237,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Glückwunsch!", emote: "🤝", asset: "assets/loot/13-haendedruck.jpg", - excludeFromInventory: true, + excludeFromInventory: true }, [LootKindId.ERLEUCHTUNG]: { id: LootKindId.ERLEUCHTUNG, @@ -247,17 +248,17 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "💡", asset: null, excludeFromInventory: true, - onDrop: async (_context, winner, channel, _loot) => { + onDrop: async(_context, winner, channel, _loot) => { const erleuchtungService = await import("./erleuchtung.js"); - const { setTimeout } = await import("node:timers/promises"); + const {setTimeout} = await import("node:timers/promises"); await setTimeout(3000); const embed = await erleuchtungService.getInspirationsEmbed(winner); await channel.send({ - embeds: [embed], + embeds: [embed] }); - }, + } }, [LootKindId.BAN]: { id: LootKindId.BAN, @@ -268,7 +269,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "🔨", asset: "assets/loot/15-ban.jpg", excludeFromInventory: true, - onDrop: async (context, winner, _channel, _loot) => { + onDrop: async(context, winner, _channel, _loot) => { const banService = await import("./ban.js"); await banService.banUser( context, @@ -276,9 +277,9 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { context.client.user, "Willkürban aus der Lotterie", false, - 0.08, + 0.08 ); - }, + } }, [LootKindId.OETTINGER]: { id: LootKindId.OETTINGER, @@ -288,7 +289,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Ja dann Prost ne!", emote: "🍺", asset: "assets/loot/16-oettinger.jpg", - gameEquip: gameItems.oettinger, + gameEquip: fightTemplates.oettinger }, [LootKindId.ACHIEVEMENT]: { id: LootKindId.ACHIEVEMENT, @@ -297,7 +298,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Ein Achievement", dropDescription: "Das erreichen echt nicht viele", emote: "🏆", - asset: "assets/loot/17-achievement.png", + asset: "assets/loot/17-achievement.png" }, [LootKindId.GME_AKTIE]: { id: LootKindId.GME_AKTIE, @@ -306,7 +307,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine wertlose GME-Aktie", dropDescription: "Der squeeze kommt bald!", emote: "📉", - asset: "assets/loot/18-gme.jpg", + asset: "assets/loot/18-gme.jpg" }, [LootKindId.FERRIS]: { id: LootKindId.FERRIS, @@ -315,16 +316,16 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Ferris - Die Krabbe", dropDescription: "Damit kann man ja endlich den Bot in Rust neuschreiben", emote: "🦀", - asset: "assets/loot/19-ferris.png", + asset: "assets/loot/19-ferris.png" }, [LootKindId.HOMEPOD]: { id: LootKindId.HOMEPOD, weight: 5, displayName: "HomePod", titleText: "Einen Apple:registered: HomePod:copyright:", - dropDescription: 'Damit dein "Smart Home" nicht mehr ganz so smart ist', + dropDescription: "Damit dein \"Smart Home\" nicht mehr ganz so smart ist", emote: "🍎", - asset: "assets/loot/20-homepod.jpg", + asset: "assets/loot/20-homepod.jpg" }, [LootKindId.RADIOACTIVE_WASTE]: { id: LootKindId.RADIOACTIVE_WASTE, @@ -335,7 +336,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { "Sollte dir ja nichts mehr anhaben, du bist ja durch den Server schon genug verstrahlt 🤷‍♂️", emote: "☢️", asset: "assets/loot/21-radioaktiver-muell.jpg", - effects: ["` +5% ` Chance auf leeres Geschenk 🔴"], + effects: ["` +5% ` Chance auf leeres Geschenk 🔴"] }, [LootKindId.SAHNE]: { id: LootKindId.SAHNE, @@ -344,7 +345,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Sprühsahne", dropDescription: "Fürs Frühstück oder so", emote: ":sahne:", - asset: "assets/loot/22-sahne.jpg", + asset: "assets/loot/22-sahne.jpg" }, [LootKindId.AEHRE]: { id: LootKindId.AEHRE, @@ -356,10 +357,10 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: ":aehre:", asset: "assets/loot/23-ehre.jpg", excludeFromInventory: true, - onDrop: async (_context, winner, _channel, _loot) => { + onDrop: async(_context, winner, _channel, _loot) => { const ehre = await import("@/storage/ehre.js"); await ehre.addPoints(winner.id, 1); - }, + } }, [LootKindId.CROWDSTRIKE]: { id: LootKindId.CROWDSTRIKE, @@ -368,7 +369,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Crowdstrike Falcon Installation", dropDescription: "Bitti nicht rebooti und Bitlocki nutzi", emote: ":eagle:", - asset: "assets/loot/24-crowdstrike.jpg", + asset: "assets/loot/24-crowdstrike.jpg" }, [LootKindId.POWERADE_BLAU]: { id: LootKindId.POWERADE_BLAU, @@ -376,7 +377,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { displayName: "Blaue Powerade", titleText: "Blaue Powerade", dropDescription: "Erfrischend erquickend. Besonders mit Vodka. Oder Korn.", - asset: "assets/loot/25-powerade-blau.jpg", + asset: "assets/loot/25-powerade-blau.jpg" }, [LootKindId.GAULOISES_BLAU]: { id: LootKindId.GAULOISES_BLAU, @@ -386,7 +387,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Rauchig, kräftig, französisch. Wie du in deinen Träumen.\n\nVerursacht Herzanfälle, genau wie dieser Server", emote: "🚬", - asset: "assets/loot/26-gauloises-blau.png", + asset: "assets/loot/26-gauloises-blau.png" }, [LootKindId.MAXWELL]: { id: LootKindId.MAXWELL, @@ -395,7 +396,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Maxwell", dropDescription: "Der ist doch tot, oder?", emote: "😸", - asset: "assets/loot/27-maxwell.gif", + asset: "assets/loot/27-maxwell.gif" }, [LootKindId.SCHICHTBEGINN_ASSE_2]: { id: LootKindId.SCHICHTBEGINN_ASSE_2, @@ -406,10 +407,10 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "🔒", asset: "assets/loot/28-asse-2.jpg", excludeFromInventory: true, - onDrop: async (context, winner, channel, _loot) => { + onDrop: async(context, winner, channel, _loot) => { const lootRoles = await import("./lootRoles.js"); await lootRoles.startAsseGuardShift(context, winner, channel); - }, + } }, [LootKindId.DRECK]: { id: LootKindId.DRECK, @@ -418,7 +419,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Ein Glas Dreck", dropDescription: "Ich hab ein Glas voll Dreck", emote: ":jar:", - asset: "assets/loot/29-dirt.jpg", + asset: "assets/loot/29-dirt.jpg" }, [LootKindId.EI]: { id: LootKindId.EI, @@ -428,7 +429,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Jetzt wär geklärt, was zu erst da war, Ei oder ... (Ja was schlüpft daraus eigentlich?)", emote: ":egg:", - asset: "assets/loot/30-egg.jpg", + asset: "assets/loot/30-egg.jpg" }, [LootKindId.BRAVO]: { id: LootKindId.BRAVO, @@ -437,7 +438,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine Bravo vom Dachboden", dropDescription: "Die Seiten kleben noch ein bisschen", emote: ":newspaper2:", - asset: "assets/loot/31-bravo.jpg", + asset: "assets/loot/31-bravo.jpg" }, [LootKindId.VERSCHIMMELTER_DOENER]: { id: LootKindId.VERSCHIMMELTER_DOENER, @@ -446,7 +447,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen verschimmelten Döner", dropDescription: "Du hättest ihn früher essen sollen", emote: "🥙", - asset: null, + asset: null }, [LootKindId.THUNFISCHSHAKE]: { id: LootKindId.THUNFISCHSHAKE, @@ -456,6 +457,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Nach Rezept zubereitet, bestehend aus Thunfisch und Reiswaffeln", emote: ":baby_bottle:", asset: "assets/loot/32-thunfischshake.jpg", + gameEquip: fightTemplates.thunfischshake }, [LootKindId.KAFFEEMUEHLE]: { id: LootKindId.KAFFEEMUEHLE, @@ -464,8 +466,8 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine Kaffeemühle für 400€", dropDescription: "Kann Kaffee mühlen. Und das gut. Mit Gold.", emote: ":coffee:", - asset: "assets/loot/34-kaffeemuehle.jpg", - }, + asset: "assets/loot/34-kaffeemuehle.jpg" + } }; export const lootTemplates: LootTemplate[] = Object.values(lootTemplateMap); @@ -491,35 +493,35 @@ export const lootAttributeTemplates: LootAttributeTemplate[] = [ classId: LootAttributeClassId.RARITY, displayName: "Normal", shortDisplay: "", - initialDropWeight: 90, + initialDropWeight: 90 }, { id: LootAttributeKindId.RARITY_RARE, classId: LootAttributeClassId.RARITY, displayName: "Selten", shortDisplay: "⭐", - initialDropWeight: 10, + initialDropWeight: 10 }, { id: LootAttributeKindId.RARITY_VERY_RARE, classId: LootAttributeClassId.RARITY, displayName: "Sehr Selten", shortDisplay: "🌟", - initialDropWeight: 1, + initialDropWeight: 1 }, { id: LootAttributeKindId.RADIOACTIVE, classId: LootAttributeClassId.OTHER, displayName: "Verstrahlt", shortDisplay: "☢️", - color: 0xff_ff_ff, + color: 0xff_ff_ff }, { id: LootAttributeKindId.SWEET, classId: LootAttributeClassId.OTHER, displayName: "Süß", - shortDisplay: "🍬", - }, + shortDisplay: "🍬" + } ]; export function resolveLootTemplate(lootKindId: number) { @@ -536,7 +538,7 @@ export function getEmote(guild: Guild, item: Loot) { } export function extractRarityAttribute( - attributes: readonly Readonly[], + attributes: readonly Readonly[] ): Readonly | undefined { const attribute = attributes.filter(a => a.attributeClassId === LootAttributeClassId.RARITY); if (attribute.length === 0) { @@ -549,14 +551,14 @@ export function extractRarityAttribute( } export function extractNonRarityAttributes( - attributes: readonly Readonly[], + attributes: readonly Readonly[] ): Readonly[] { return attributes.filter(a => a.attributeClassId !== LootAttributeClassId.RARITY); } export function itemHasAttribute( attributes: readonly Readonly[], - kindId: LootAttributeKindId, + kindId: LootAttributeKindId ): boolean { return attributes.some(a => a.attributeKindId === kindId); } From 18e55c0a59e14f9248d3ad7a35cefbf12a3096c5 Mon Sep 17 00:00:00 2001 From: Feluin <20358156+feluin@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:28:48 +0100 Subject: [PATCH 14/26] fixed multiselection --- src/commands/fight.ts | 20 ++++-- src/commands/gegenstand.ts | 112 ++++++++++++++++---------------- src/service/fightData.ts | 38 +++++------ src/service/lootData.ts | 128 ++++++++++++++++++------------------- 4 files changed, 152 insertions(+), 146 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 371f0d0f..ac788ba7 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,4 +1,4 @@ -import type {ApplicationCommand} from "@/commands/command.js"; +import type { ApplicationCommand } from "@/commands/command.js"; import { type APIEmbed, APIEmbedField, @@ -7,13 +7,19 @@ import { type CommandInteraction, type InteractionResponse, SlashCommandBuilder, - User + User, } from "discord.js"; -import type {BotContext} from "@/context.js"; -import type {JSONEncodable} from "@discordjs/util"; -import {type BaseEntity, baseStats, bossMap, Entity, type FightScene} from "@/service/fightData.js"; -import {setTimeout} from "node:timers/promises"; -import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; +import type { BotContext } from "@/context.js"; +import type { JSONEncodable } from "@discordjs/util"; +import { + type BaseEntity, + baseStats, + bossMap, + Entity, + type FightScene, +} from "@/service/fightData.js"; +import { setTimeout } from "node:timers/promises"; +import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; async function getFighter(user: User): Promise { const userInventory = await getFightInventoryEnriched(user.id); diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index 46f647ba..de45a897 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -7,24 +7,24 @@ import { type CommandInteraction, SlashCommandBuilder, SlashCommandStringOption, - SlashCommandSubcommandBuilder + SlashCommandSubcommandBuilder, } from "discord.js"; import * as sentry from "@sentry/bun"; -import type {BotContext} from "@/context.js"; -import type {ApplicationCommand} from "@/commands/command.js"; -import type {LootUseCommandInteraction} from "@/storage/loot.js"; +import type { BotContext } from "@/context.js"; +import type { ApplicationCommand } from "@/commands/command.js"; +import type { LootUseCommandInteraction } from "@/storage/loot.js"; import * as lootService from "@/service/loot.js"; import * as lootRoleService from "@/service/lootRoles.js"; -import {randomEntry} from "@/utils/arrayUtils.js"; -import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; +import { randomEntry } from "@/utils/arrayUtils.js"; +import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; import * as imageService from "@/service/image.js"; import * as lootDataService from "@/service/lootData.js"; -import {LootAttributeClassId, LootAttributeKindId, LootKindId} from "@/service/lootData.js"; +import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; import log from "@log"; -import {equipItembyLoot, getFightInventoryUnsorted} from "@/storage/fightinventory.js"; +import { equipItembyLoot, getFightInventoryUnsorted } from "@/storage/fightinventory.js"; export default class GegenstandCommand implements ApplicationCommand { name = "gegenstand"; @@ -36,7 +36,7 @@ export default class GegenstandCommand implements ApplicationCommand { .addSubcommand( new SlashCommandSubcommandBuilder() .setName("entsorgen") - .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes") + .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes"), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -47,8 +47,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Der Gegenstand, über den du Informationen haben möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -59,8 +59,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du benutzen möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -71,8 +71,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Rüste dich für deinen nächsten Kampf") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -104,33 +104,33 @@ export default class GegenstandCommand implements ApplicationCommand { { description: "Es ist kein Wärter im Dienst. Das Tor ist zu. Du rennst dagegen. Opfer.", - color: 0xff0000 - } - ] + color: 0xff0000, + }, + ], }); return; } const wasteContents = await lootService.getUserLootsByTypeId( interaction.user.id, - LootKindId.RADIOACTIVE_WASTE + LootKindId.RADIOACTIVE_WASTE, ); if (wasteContents.length === 0) { await interaction.reply({ - content: "Du hast keinen Atommüll, den du in die Grube werfen kannst." + content: "Du hast keinen Atommüll, den du in die Grube werfen kannst.", }); return; } const sweetContent = await lootService.getUserLootsWithAttribute( interaction.user.id, - LootAttributeKindId.SWEET + LootAttributeKindId.SWEET, ); if (sweetContent.length === 0) { await interaction.reply({ - content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst." + content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst.", }); return; } @@ -142,7 +142,7 @@ export default class GegenstandCommand implements ApplicationCommand { const messages = [ `Du hast dem Wärter ${currentGuard} etwas Atommüll und etwas Süßes zum Naschen gegeben.`, `${currentGuard} hat sich über deinen Atommüll und die süßen Sachen gefreut.`, - `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.` + `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.`, ]; await interaction.reply({ @@ -151,11 +151,11 @@ export default class GegenstandCommand implements ApplicationCommand { title: "Atommüll entsorgt!", description: randomEntry(messages), footer: { - text: "Jetzt ist es das Problem des deutschen Steuerzahlers" + text: "Jetzt ist es das Problem des deutschen Steuerzahlers", }, - color: 0x00ff00 - } - ] + color: 0x00ff00, + }, + ], }); } @@ -172,7 +172,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const {item, template, attributes} = info; + const { item, template, attributes } = info; const effects = template.effects ?? []; @@ -202,14 +202,14 @@ export default class GegenstandCommand implements ApplicationCommand { const extraFields: (APIEmbedField | undefined)[] = [ template.onUse !== undefined - ? {name: "🔧 Benutzbar", value: "", inline: true} + ? { name: "🔧 Benutzbar", value: "", inline: true } : undefined, ...otherAttributes.map(attribute => ({ name: `${attribute.shortDisplay} ${attribute.displayName}`.trim(), value: "", - inline: true - })) + inline: true, + })), ]; await interaction.reply({ @@ -220,31 +220,31 @@ export default class GegenstandCommand implements ApplicationCommand { color: 0x00ff00, image: attachment ? { - url: "attachment://hero.gif", - width: 128 - } + url: "attachment://hero.gif", + width: 128, + } : undefined, fields: [ ...effects.map(value => ({ name: "⚡️ Effekt", value, - inline: true + inline: true, })), - ...extraFields.filter(e => e !== undefined) + ...extraFields.filter(e => e !== undefined), ], footer: { - text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim() - } - } + text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim(), + }, + }, ], files: attachment ? [ - { - name: "hero.gif", - attachment - } - ] - : [] + { + name: "hero.gif", + attachment, + }, + ] + : [], }); } @@ -261,12 +261,12 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const {item, template} = info; + const { item, template } = info; if (template.onUse === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht benutzt werden.", - ephemeral: true + ephemeral: true, }); return; } @@ -276,14 +276,14 @@ export default class GegenstandCommand implements ApplicationCommand { keepInInventory = await template.onUse( interaction as LootUseCommandInteraction, context, - item + item, ); } catch (error) { log.error(error, "Error while using item"); sentry.captureException(error); await interaction.reply({ content: "Beim Benutzen dieses Gegenstands ist ein Fehler aufgetreten.", - ephemeral: true + ephemeral: true, }); } @@ -302,7 +302,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (!item) { await interaction.reply({ content: "Diesen Gegensand hast du nicht.", - ephemeral: true + ephemeral: true, }); return; } @@ -311,14 +311,14 @@ export default class GegenstandCommand implements ApplicationCommand { if (!template) { await interaction.reply({ content: "Dieser Gegenstand ist unbekannt.", - ephemeral: true + ephemeral: true, }); return; } const attributes = await lootService.getLootAttributes(item.id); - return {item, template, attributes}; + return { item, template, attributes }; } async autocomplete(interaction: AutocompleteInteraction) { @@ -362,7 +362,7 @@ export default class GegenstandCommand implements ApplicationCommand { typeof emote === "string" ? `${emote} ${item.displayName} (${item.id})` : `${item.displayName} (${item.id})`, - value: String(item.id) + value: String(item.id), }); } @@ -381,12 +381,12 @@ export default class GegenstandCommand implements ApplicationCommand { if (!info) { return; } - const {item, template} = info; + const { item, template } = info; log.info(item); if (template.gameEquip === undefined) { await interaction.reply({ content: ` ${item.displayName} kann nicht ausgerüstet werden.`, - ephemeral: true + ephemeral: true, }); return; } @@ -394,7 +394,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (items.filter(i => i.id == item.id)) { await interaction.reply({ content: `Du hast ${item.displayName} schon ausgerüstet`, - ephemeral: true + ephemeral: true, }); return; } diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 9bbd345b..69b8f7f9 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,37 +1,37 @@ export const fightTemplates: { [name: string]: Equipable } = { ayran: { type: "item", - attackModifier: {min: 2, max: 3} + attackModifier: { min: 2, max: 3 }, }, oettinger: { type: "item", - attackModifier: {min: 1, max: 5}, - defenceModifier: {min: -3, max: 0} + attackModifier: { min: 1, max: 5 }, + defenceModifier: { min: -3, max: 0 }, }, thunfischshake: { type: "item", - attackModifier: {min: 3, max: 5} + attackModifier: { min: 3, max: 5 }, }, nachthemd: { type: "armor", health: 50, - defence: {min: 2, max: 5} + defence: { min: 2, max: 5 }, }, eierwaermer: { type: "armor", health: 30, - defence: {min: 3, max: 5} + defence: { min: 3, max: 5 }, }, dildo: { type: "weapon", - attack: {min: 3, max: 9} + attack: { min: 3, max: 9 }, }, messerblock: { type: "weapon", - attack: {min: 1, max: 9} - } + attack: { min: 1, max: 9 }, + }, }; export const bossMap: { [name: string]: BaseEntity } = { gudrun: { @@ -42,7 +42,7 @@ export const bossMap: { [name: string]: BaseEntity } = { baseDefence: 0, armor: fightTemplates.nachthemd as EquipableArmor, weapon: fightTemplates.dildo as EquipableWeapon, - items: [] + items: [], }, deinchef: { @@ -51,7 +51,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [] + items: [], }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -59,7 +59,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [] + items: [], }, rentner: { name: "Reeeeeeentner", @@ -67,7 +67,7 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 200, baseDamage: 3, baseDefence: 5, - items: [] + items: [], }, barkeeper: { name: "Barkeeper aus Nürnia", @@ -77,15 +77,15 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 350, baseDamage: 5, baseDefence: 5, - items: [] - } + items: [], + }, }; export const baseStats = { description: "", health: 80, baseDamage: 1, - baseDefence: 0 + baseDefence: 0, }; export type FightItemType = "weapon" | "armor" | "item"; @@ -148,7 +148,7 @@ export class Entity { const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( - `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}` + `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -189,7 +189,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; + return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; } - return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; + return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; } diff --git a/src/service/lootData.ts b/src/service/lootData.ts index a2656658..96b15a68 100644 --- a/src/service/lootData.ts +++ b/src/service/lootData.ts @@ -1,12 +1,12 @@ -import type {LootAttributeTemplate, LootTemplate} from "@/storage/loot.js"; +import type { LootAttributeTemplate, LootTemplate } from "@/storage/loot.js"; import * as lootDropService from "@/service/lootDrop.js"; import * as emoteService from "@/service/emote.js"; -import {GuildMember, type Guild} from "discord.js"; -import type {Loot, LootAttribute} from "@/storage/db/model.js"; +import { GuildMember, type Guild } from "discord.js"; +import type { Loot, LootAttribute } from "@/storage/db/model.js"; import log from "@log"; -import {fightTemplates} from "@/service/fightData.js"; +import { fightTemplates } from "@/service/fightData.js"; const ACHTUNG_NICHT_DROPBAR_WEIGHT_KG = 0; @@ -73,7 +73,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "✨Nichts✨", dropDescription: "¯\\_(ツ)_/¯", asset: null, - excludeFromInventory: true + excludeFromInventory: true, }, [LootKindId.KADSE]: { id: LootKindId.KADSE, @@ -84,8 +84,8 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: ":catsmile:", asset: "assets/loot/01-kadse.jpg", attributeAsset: { - [LootAttributeKindId.RADIOACTIVE]: "assets/loot/attributes/01-kadse-verstrahlt.jpg" - } + [LootAttributeKindId.RADIOACTIVE]: "assets/loot/attributes/01-kadse-verstrahlt.jpg", + }, }, [LootKindId.MESSERBLOCK]: { id: LootKindId.MESSERBLOCK, @@ -95,7 +95,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "🔪", emote: "🔪", asset: "assets/loot/02-messerblock.jpg", - gameEquip: fightTemplates.messerblock + gameEquip: fightTemplates.messerblock, }, [LootKindId.KUEHLSCHRANK]: { id: LootKindId.KUEHLSCHRANK, @@ -106,7 +106,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { "Dafür haben wir keine Kosten und Mühen gescheut und extra einen Kredit aufgenommen.", emote: "🧊", asset: "assets/loot/03-kuehlschrank.jpg", - effects: ["Lässt Essen nicht schimmeln"] + effects: ["Lässt Essen nicht schimmeln"], }, [LootKindId.DOENER]: { id: LootKindId.DOENER, @@ -115,7 +115,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Döner", dropDescription: "Bewahre ihn gut als Geldanlage auf!", emote: "🥙", - asset: "assets/loot/04-doener.jpg" + asset: "assets/loot/04-doener.jpg", }, [LootKindId.KINN]: { id: LootKindId.KINN, @@ -124,7 +124,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Ein Kinn", dropDescription: "Pass gut drauf auf, sonst flieht es!", emote: "👶", - asset: "assets/loot/05-kinn.jpg" + asset: "assets/loot/05-kinn.jpg", }, [LootKindId.KRANKSCHREIBUNG]: { id: LootKindId.KRANKSCHREIBUNG, @@ -136,7 +136,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { "Mit der Krankschreibung kannst du deine Wärterschicht abbrechen und dich ausruhen.", emote: "🩺", asset: "assets/loot/06-krankschreibung.jpg", - onUse: async(interaction, context, loot) => { + onUse: async (interaction, context, loot) => { const lootRoles = await import("./lootRoles.js"); const member = interaction.member; if (!member || !(member instanceof GuildMember)) { @@ -146,18 +146,18 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { if (!isOnDuty) { await interaction.reply( - "Du bist gar nicht im Dienst, aber hast dir deinen Urlaub trotzdem wohl verdient." + "Du bist gar nicht im Dienst, aber hast dir deinen Urlaub trotzdem wohl verdient.", ); return false; } await lootRoles.endAsseGuardShift(context, member); await interaction.reply( - "Du hast kränkelnd beim Werksleiter angerufen und dich krankgemeldet. Genieße deinen Tag zu Hause!" + "Du hast kränkelnd beim Werksleiter angerufen und dich krankgemeldet. Genieße deinen Tag zu Hause!", ); return false; - } + }, }, [LootKindId.WUERFELWURF]: { id: LootKindId.WUERFELWURF, @@ -168,10 +168,10 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "🎲", asset: "assets/loot/07-wuerfelwurf.jpg", excludeFromInventory: true, - onDrop: async(_content, winner, channel, _loot) => { + onDrop: async (_content, winner, channel, _loot) => { const rollService = await import("./roll.js"); await rollService.rollInChannel(winner.user, channel, 1, 6); - } + }, }, [LootKindId.GESCHENK]: { id: LootKindId.GESCHENK, @@ -181,15 +181,15 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Du kannst jemand anderem eine Freude machen :feelsamazingman:", emote: "🎁", asset: null, - onUse: async(interaction, context, loot) => { + onUse: async (interaction, context, loot) => { await lootDropService.postLootDrop( context, interaction.channel, interaction.user, - loot.id + loot.id, ); return false; - } + }, }, [LootKindId.AYRAN]: { id: LootKindId.AYRAN, @@ -199,7 +199,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Der gute von Müller", emote: "🥛", asset: "assets/loot/09-ayran.jpg", - gameEquip: fightTemplates.ayran + gameEquip: fightTemplates.ayran, }, [LootKindId.PKV]: { id: LootKindId.PKV, @@ -209,7 +209,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Fehlt dir nur noch das Geld zum Vorstrecken", emote: "💉", asset: "assets/loot/10-pkv.jpg", - effects: ["` +100% ` Chance auf AU 🟢"] + effects: ["` +100% ` Chance auf AU 🟢"], }, [LootKindId.TRICHTER]: { id: LootKindId.TRICHTER, @@ -218,7 +218,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Trichter", dropDescription: "Für die ganz großen Schlücke", emote: ":trichter:", - asset: "assets/loot/11-trichter.jpg" + asset: "assets/loot/11-trichter.jpg", }, [LootKindId.GRAFIKKARTE]: { id: LootKindId.GRAFIKKARTE, @@ -227,7 +227,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine Grafikkarte aus der Zukunft", dropDescription: "Leider ohne Treiber, die gibt es erst in 3 Monaten", emote: "🖥️", - asset: "assets/loot/12-grafikkarte.png" + asset: "assets/loot/12-grafikkarte.png", }, [LootKindId.HAENDEDRUCK]: { id: LootKindId.HAENDEDRUCK, @@ -237,7 +237,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Glückwunsch!", emote: "🤝", asset: "assets/loot/13-haendedruck.jpg", - excludeFromInventory: true + excludeFromInventory: true, }, [LootKindId.ERLEUCHTUNG]: { id: LootKindId.ERLEUCHTUNG, @@ -248,17 +248,17 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "💡", asset: null, excludeFromInventory: true, - onDrop: async(_context, winner, channel, _loot) => { + onDrop: async (_context, winner, channel, _loot) => { const erleuchtungService = await import("./erleuchtung.js"); - const {setTimeout} = await import("node:timers/promises"); + const { setTimeout } = await import("node:timers/promises"); await setTimeout(3000); const embed = await erleuchtungService.getInspirationsEmbed(winner); await channel.send({ - embeds: [embed] + embeds: [embed], }); - } + }, }, [LootKindId.BAN]: { id: LootKindId.BAN, @@ -269,7 +269,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "🔨", asset: "assets/loot/15-ban.jpg", excludeFromInventory: true, - onDrop: async(context, winner, _channel, _loot) => { + onDrop: async (context, winner, _channel, _loot) => { const banService = await import("./ban.js"); await banService.banUser( context, @@ -277,9 +277,9 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { context.client.user, "Willkürban aus der Lotterie", false, - 0.08 + 0.08, ); - } + }, }, [LootKindId.OETTINGER]: { id: LootKindId.OETTINGER, @@ -289,7 +289,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Ja dann Prost ne!", emote: "🍺", asset: "assets/loot/16-oettinger.jpg", - gameEquip: fightTemplates.oettinger + gameEquip: fightTemplates.oettinger, }, [LootKindId.ACHIEVEMENT]: { id: LootKindId.ACHIEVEMENT, @@ -298,7 +298,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Ein Achievement", dropDescription: "Das erreichen echt nicht viele", emote: "🏆", - asset: "assets/loot/17-achievement.png" + asset: "assets/loot/17-achievement.png", }, [LootKindId.GME_AKTIE]: { id: LootKindId.GME_AKTIE, @@ -307,7 +307,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine wertlose GME-Aktie", dropDescription: "Der squeeze kommt bald!", emote: "📉", - asset: "assets/loot/18-gme.jpg" + asset: "assets/loot/18-gme.jpg", }, [LootKindId.FERRIS]: { id: LootKindId.FERRIS, @@ -316,16 +316,16 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Ferris - Die Krabbe", dropDescription: "Damit kann man ja endlich den Bot in Rust neuschreiben", emote: "🦀", - asset: "assets/loot/19-ferris.png" + asset: "assets/loot/19-ferris.png", }, [LootKindId.HOMEPOD]: { id: LootKindId.HOMEPOD, weight: 5, displayName: "HomePod", titleText: "Einen Apple:registered: HomePod:copyright:", - dropDescription: "Damit dein \"Smart Home\" nicht mehr ganz so smart ist", + dropDescription: 'Damit dein "Smart Home" nicht mehr ganz so smart ist', emote: "🍎", - asset: "assets/loot/20-homepod.jpg" + asset: "assets/loot/20-homepod.jpg", }, [LootKindId.RADIOACTIVE_WASTE]: { id: LootKindId.RADIOACTIVE_WASTE, @@ -336,7 +336,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { "Sollte dir ja nichts mehr anhaben, du bist ja durch den Server schon genug verstrahlt 🤷‍♂️", emote: "☢️", asset: "assets/loot/21-radioaktiver-muell.jpg", - effects: ["` +5% ` Chance auf leeres Geschenk 🔴"] + effects: ["` +5% ` Chance auf leeres Geschenk 🔴"], }, [LootKindId.SAHNE]: { id: LootKindId.SAHNE, @@ -345,7 +345,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Sprühsahne", dropDescription: "Fürs Frühstück oder so", emote: ":sahne:", - asset: "assets/loot/22-sahne.jpg" + asset: "assets/loot/22-sahne.jpg", }, [LootKindId.AEHRE]: { id: LootKindId.AEHRE, @@ -357,10 +357,10 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: ":aehre:", asset: "assets/loot/23-ehre.jpg", excludeFromInventory: true, - onDrop: async(_context, winner, _channel, _loot) => { + onDrop: async (_context, winner, _channel, _loot) => { const ehre = await import("@/storage/ehre.js"); await ehre.addPoints(winner.id, 1); - } + }, }, [LootKindId.CROWDSTRIKE]: { id: LootKindId.CROWDSTRIKE, @@ -369,7 +369,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Crowdstrike Falcon Installation", dropDescription: "Bitti nicht rebooti und Bitlocki nutzi", emote: ":eagle:", - asset: "assets/loot/24-crowdstrike.jpg" + asset: "assets/loot/24-crowdstrike.jpg", }, [LootKindId.POWERADE_BLAU]: { id: LootKindId.POWERADE_BLAU, @@ -377,7 +377,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { displayName: "Blaue Powerade", titleText: "Blaue Powerade", dropDescription: "Erfrischend erquickend. Besonders mit Vodka. Oder Korn.", - asset: "assets/loot/25-powerade-blau.jpg" + asset: "assets/loot/25-powerade-blau.jpg", }, [LootKindId.GAULOISES_BLAU]: { id: LootKindId.GAULOISES_BLAU, @@ -387,7 +387,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Rauchig, kräftig, französisch. Wie du in deinen Träumen.\n\nVerursacht Herzanfälle, genau wie dieser Server", emote: "🚬", - asset: "assets/loot/26-gauloises-blau.png" + asset: "assets/loot/26-gauloises-blau.png", }, [LootKindId.MAXWELL]: { id: LootKindId.MAXWELL, @@ -396,7 +396,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen Maxwell", dropDescription: "Der ist doch tot, oder?", emote: "😸", - asset: "assets/loot/27-maxwell.gif" + asset: "assets/loot/27-maxwell.gif", }, [LootKindId.SCHICHTBEGINN_ASSE_2]: { id: LootKindId.SCHICHTBEGINN_ASSE_2, @@ -407,10 +407,10 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { emote: "🔒", asset: "assets/loot/28-asse-2.jpg", excludeFromInventory: true, - onDrop: async(context, winner, channel, _loot) => { + onDrop: async (context, winner, channel, _loot) => { const lootRoles = await import("./lootRoles.js"); await lootRoles.startAsseGuardShift(context, winner, channel); - } + }, }, [LootKindId.DRECK]: { id: LootKindId.DRECK, @@ -419,7 +419,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Ein Glas Dreck", dropDescription: "Ich hab ein Glas voll Dreck", emote: ":jar:", - asset: "assets/loot/29-dirt.jpg" + asset: "assets/loot/29-dirt.jpg", }, [LootKindId.EI]: { id: LootKindId.EI, @@ -429,7 +429,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Jetzt wär geklärt, was zu erst da war, Ei oder ... (Ja was schlüpft daraus eigentlich?)", emote: ":egg:", - asset: "assets/loot/30-egg.jpg" + asset: "assets/loot/30-egg.jpg", }, [LootKindId.BRAVO]: { id: LootKindId.BRAVO, @@ -438,7 +438,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine Bravo vom Dachboden", dropDescription: "Die Seiten kleben noch ein bisschen", emote: ":newspaper2:", - asset: "assets/loot/31-bravo.jpg" + asset: "assets/loot/31-bravo.jpg", }, [LootKindId.VERSCHIMMELTER_DOENER]: { id: LootKindId.VERSCHIMMELTER_DOENER, @@ -447,7 +447,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Einen verschimmelten Döner", dropDescription: "Du hättest ihn früher essen sollen", emote: "🥙", - asset: null + asset: null, }, [LootKindId.THUNFISCHSHAKE]: { id: LootKindId.THUNFISCHSHAKE, @@ -457,7 +457,7 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { dropDescription: "Nach Rezept zubereitet, bestehend aus Thunfisch und Reiswaffeln", emote: ":baby_bottle:", asset: "assets/loot/32-thunfischshake.jpg", - gameEquip: fightTemplates.thunfischshake + gameEquip: fightTemplates.thunfischshake, }, [LootKindId.KAFFEEMUEHLE]: { id: LootKindId.KAFFEEMUEHLE, @@ -466,8 +466,8 @@ export const lootTemplateMap: { [id: string]: LootTemplate } = { titleText: "Eine Kaffeemühle für 400€", dropDescription: "Kann Kaffee mühlen. Und das gut. Mit Gold.", emote: ":coffee:", - asset: "assets/loot/34-kaffeemuehle.jpg" - } + asset: "assets/loot/34-kaffeemuehle.jpg", + }, }; export const lootTemplates: LootTemplate[] = Object.values(lootTemplateMap); @@ -493,35 +493,35 @@ export const lootAttributeTemplates: LootAttributeTemplate[] = [ classId: LootAttributeClassId.RARITY, displayName: "Normal", shortDisplay: "", - initialDropWeight: 90 + initialDropWeight: 90, }, { id: LootAttributeKindId.RARITY_RARE, classId: LootAttributeClassId.RARITY, displayName: "Selten", shortDisplay: "⭐", - initialDropWeight: 10 + initialDropWeight: 10, }, { id: LootAttributeKindId.RARITY_VERY_RARE, classId: LootAttributeClassId.RARITY, displayName: "Sehr Selten", shortDisplay: "🌟", - initialDropWeight: 1 + initialDropWeight: 1, }, { id: LootAttributeKindId.RADIOACTIVE, classId: LootAttributeClassId.OTHER, displayName: "Verstrahlt", shortDisplay: "☢️", - color: 0xff_ff_ff + color: 0xff_ff_ff, }, { id: LootAttributeKindId.SWEET, classId: LootAttributeClassId.OTHER, displayName: "Süß", - shortDisplay: "🍬" - } + shortDisplay: "🍬", + }, ]; export function resolveLootTemplate(lootKindId: number) { @@ -538,7 +538,7 @@ export function getEmote(guild: Guild, item: Loot) { } export function extractRarityAttribute( - attributes: readonly Readonly[] + attributes: readonly Readonly[], ): Readonly | undefined { const attribute = attributes.filter(a => a.attributeClassId === LootAttributeClassId.RARITY); if (attribute.length === 0) { @@ -551,14 +551,14 @@ export function extractRarityAttribute( } export function extractNonRarityAttributes( - attributes: readonly Readonly[] + attributes: readonly Readonly[], ): Readonly[] { return attributes.filter(a => a.attributeClassId !== LootAttributeClassId.RARITY); } export function itemHasAttribute( attributes: readonly Readonly[], - kindId: LootAttributeKindId + kindId: LootAttributeKindId, ): boolean { return attributes.some(a => a.attributeKindId === kindId); } From 0bf435417c71cd2260bb3f9b7405ef0fd4af3fee Mon Sep 17 00:00:00 2001 From: Feluin Date: Thu, 2 Jan 2025 18:19:31 +0100 Subject: [PATCH 15/26] added history and stuff --- src/commands/fight.ts | 151 ++++++++++++++++++----- src/commands/gegenstand.ts | 116 ++++++++--------- src/commands/inventar.ts | 86 ++++++------- src/service/fightData.ts | 90 +++++++++----- src/storage/db/model.ts | 3 +- src/storage/fighthistory.ts | 36 ++++++ src/storage/fightinventory.ts | 50 +++++--- src/storage/migrations/11-fightsystem.ts | 1 + 8 files changed, 350 insertions(+), 183 deletions(-) create mode 100644 src/storage/fighthistory.ts diff --git a/src/commands/fight.ts b/src/commands/fight.ts index ac788ba7..b7b3219a 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,4 +1,4 @@ -import type { ApplicationCommand } from "@/commands/command.js"; +import type {ApplicationCommand} from "@/commands/command.js"; import { type APIEmbed, APIEmbedField, @@ -7,19 +7,23 @@ import { type CommandInteraction, type InteractionResponse, SlashCommandBuilder, - User, + type User } from "discord.js"; -import type { BotContext } from "@/context.js"; -import type { JSONEncodable } from "@discordjs/util"; +import type {BotContext} from "@/context.js"; +import type {JSONEncodable} from "@discordjs/util"; import { type BaseEntity, baseStats, bossMap, Entity, - type FightScene, + EquipableArmor, + EquipableItem, + EquipableWeapon, + type FightScene } from "@/service/fightData.js"; -import { setTimeout } from "node:timers/promises"; -import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; +import {setTimeout} from "node:timers/promises"; +import {getFightInventoryEnriched, removeItemsAfterFight} from "@/storage/fightinventory.js"; +import {getLastFight, insertResult} from "@/storage/fighthistory.js"; async function getFighter(user: User): Promise { const userInventory = await getFightInventoryEnriched(user.id); @@ -27,9 +31,21 @@ async function getFighter(user: User): Promise { return { ...baseStats, name: user.displayName, - weapon: userInventory.weapon, - armor: userInventory.armor, - items: userInventory.items, + weapon: { + name: userInventory.weapon?.itemInfo?.displayName ?? "Nichts", + ...userInventory.weapon?.gameTemplate as EquipableWeapon + }, + armor: { + name: userInventory.armor?.itemInfo?.displayName ?? "Nichts", + ...userInventory.armor?.gameTemplate as EquipableArmor + }, + items: userInventory.items.map(value => { + return { + name: value.itemInfo?.displayName ?? "Error", + ...value.gameTemplate as EquipableItem + }; + + }) }; } @@ -46,27 +62,55 @@ export default class FightCommand implements ApplicationCommand { .setDescription("Boss") //switch to autocomplete when we reach 25 .addChoices( - Object.entries(bossMap).map(boss => { + Object.entries(bossMap).filter(boss => boss[1].enabled).map(boss => { return { name: boss[1].name, - value: boss[0], + value: boss[0] }; - }), - ), + }) + ) ); async handleInteraction(command: CommandInteraction, context: BotContext) { const boss = command.options.get("boss", true).value as string; + + let lastFight = await getLastFight(command.user.id); + + // if (lastFight !== undefined && (new Date(lastFight?.createdAt).getTime() > new Date().getTime() - 1000 * 60 * 60 * 24 * 5)) { + // await command.reply({ + // embeds: + // [{ + // title: "Du bist noch nicht bereit", + // color: 0xe74c3c, + // description: `Dein Letzter Kampf gegen ${bossMap[lastFight.bossName]?.name} ist noch keine 5 Tage her. Gegen ${bossMap[boss].name} hast du sowieso Chance, aber noch weniger, wenn nicht die vorgeschriebenen Pausenzeiten einhältst ` + // } + // ] + // , + // ephemeral: true + // } + // ); + + // return; + // } + const interactionResponse = await command.deferReply(); - console.log(boss); - const playerstats = await getFighter(command.user); - await fight(playerstats, bossMap[boss], interactionResponse); + const + playerstats = await getFighter(command.user); + + + await + + fight(command + .user, playerstats, boss, {...bossMap[boss]}, + interactionResponse + ); + } } -type result = "PLAYER" | "ENEMY" | undefined; +type result = "PLAYER" | "ENEMY" | undefined function checkWin(fightscene: FightScene): result { if (fightscene.player.stats.health < 0) { @@ -77,17 +121,57 @@ function checkWin(fightscene: FightScene): result { } } -export async function fight( +function renderEndScreen(fightscene: FightScene): APIEmbed | JSONEncodable { + const result = checkWin(fightscene); + const fields = [ + renderStats(fightscene.player), + renderStats(fightscene.enemy), + { + name: "Verlauf", + value: " " + }, + { + name: " Die Items sind dir leider beim Kampf kaputt gegangen: ", + value: fightscene.player.stats.items.map(value => value.name).join(" \n") + } + ]; + if (result == "PLAYER") { + return { + title: `Mit viel Glück konnte ${fightscene.player.stats.name} ${fightscene.enemy.stats.name} besiegen `, + color: 0x57F287, + description: fightscene.enemy.stats.description, + fields: fields + }; + } + if (result == "ENEMY") { + return { + title: `${fightscene.enemy.stats.name} hat ${fightscene.player.stats.name} gnadenlos vernichtet`, + color: 0xED4245, + description: fightscene.enemy.stats.description, + fields: fields + }; + } + return { + title: `Kampf zwischen ${fightscene.player.stats.name} und ${fightscene.enemy.stats.name}`, + description: fightscene.enemy.stats.description, + fields: fields + }; +} + + +export async function fight(user: User, playerstats: BaseEntity, + boss: string, enemystats: BaseEntity, - interactionResponse: InteractionResponse>, + interactionResponse: + InteractionResponse> ) { const enemy = new Entity(enemystats); const player = new Entity(playerstats); const scene: FightScene = { player: player, - enemy: enemy, + enemy: enemy }; while (checkWin(scene) === undefined) { player.itemtext = []; @@ -107,28 +191,35 @@ export async function fight( if (!value.afterFight) { continue; } - value.afterFight({ player: enemy, enemy: player }); + value.afterFight({player: enemy, enemy: player}); } - await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); + await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); await setTimeout(200); } + + await interactionResponse.edit({embeds: [renderEndScreen(scene)]}); + //delete items + await removeItemsAfterFight(user.id); + // + await insertResult(user.id, boss, checkWin(scene) == "PLAYER"); + + } function renderStats(player: Entity) { while (player.itemtext.length < 5) { player.itemtext.push("-"); } - return { name: player.stats.name, value: `❤️HP${player.stats.health}/${player.maxhealth} ❤️${"=".repeat(Math.max(0, (player.stats.health / player.maxhealth) * 10))} - ⚔️Waffe: ${player.stats.weapon ?? "Schwengel"} ${player.lastattack} - 🛡️Rüstung: ${player.stats.armor ?? "Nackt"} ${player.lastdefence} + ⚔️Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastattack} + 🛡️Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastdefence} 📚Items: ${player.itemtext.join("\n")} `, - inline: true, + inline: true }; } @@ -141,8 +232,8 @@ function renderFightEmbedded(fightscene: FightScene): JSONEncodable | renderStats(fightscene.enemy), { name: "Verlauf", - value: " ", - }, - ], + value: " " + } + ] }; } diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index de45a897..1f91cc4b 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -7,24 +7,24 @@ import { type CommandInteraction, SlashCommandBuilder, SlashCommandStringOption, - SlashCommandSubcommandBuilder, + SlashCommandSubcommandBuilder } from "discord.js"; import * as sentry from "@sentry/bun"; -import type { BotContext } from "@/context.js"; -import type { ApplicationCommand } from "@/commands/command.js"; -import type { LootUseCommandInteraction } from "@/storage/loot.js"; +import type {BotContext} from "@/context.js"; +import type {ApplicationCommand} from "@/commands/command.js"; +import type {LootUseCommandInteraction} from "@/storage/loot.js"; import * as lootService from "@/service/loot.js"; import * as lootRoleService from "@/service/lootRoles.js"; -import { randomEntry } from "@/utils/arrayUtils.js"; -import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; +import {randomEntry} from "@/utils/arrayUtils.js"; +import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; import * as imageService from "@/service/image.js"; import * as lootDataService from "@/service/lootData.js"; -import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; +import {LootAttributeClassId, LootAttributeKindId, LootKindId} from "@/service/lootData.js"; import log from "@log"; -import { equipItembyLoot, getFightInventoryUnsorted } from "@/storage/fightinventory.js"; +import {equipItembyLoot, getFightInventoryUnsorted} from "@/storage/fightinventory.js"; export default class GegenstandCommand implements ApplicationCommand { name = "gegenstand"; @@ -36,7 +36,7 @@ export default class GegenstandCommand implements ApplicationCommand { .addSubcommand( new SlashCommandSubcommandBuilder() .setName("entsorgen") - .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes"), + .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes") ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -47,8 +47,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Der Gegenstand, über den du Informationen haben möchtest") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -59,8 +59,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du benutzen möchtest") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -71,8 +71,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Rüste dich für deinen nächsten Kampf") - .setAutocomplete(true), - ), + .setAutocomplete(true) + ) ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -104,33 +104,33 @@ export default class GegenstandCommand implements ApplicationCommand { { description: "Es ist kein Wärter im Dienst. Das Tor ist zu. Du rennst dagegen. Opfer.", - color: 0xff0000, - }, - ], + color: 0xff0000 + } + ] }); return; } const wasteContents = await lootService.getUserLootsByTypeId( interaction.user.id, - LootKindId.RADIOACTIVE_WASTE, + LootKindId.RADIOACTIVE_WASTE ); if (wasteContents.length === 0) { await interaction.reply({ - content: "Du hast keinen Atommüll, den du in die Grube werfen kannst.", + content: "Du hast keinen Atommüll, den du in die Grube werfen kannst." }); return; } const sweetContent = await lootService.getUserLootsWithAttribute( interaction.user.id, - LootAttributeKindId.SWEET, + LootAttributeKindId.SWEET ); if (sweetContent.length === 0) { await interaction.reply({ - content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst.", + content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst." }); return; } @@ -142,7 +142,7 @@ export default class GegenstandCommand implements ApplicationCommand { const messages = [ `Du hast dem Wärter ${currentGuard} etwas Atommüll und etwas Süßes zum Naschen gegeben.`, `${currentGuard} hat sich über deinen Atommüll und die süßen Sachen gefreut.`, - `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.`, + `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.` ]; await interaction.reply({ @@ -151,11 +151,11 @@ export default class GegenstandCommand implements ApplicationCommand { title: "Atommüll entsorgt!", description: randomEntry(messages), footer: { - text: "Jetzt ist es das Problem des deutschen Steuerzahlers", + text: "Jetzt ist es das Problem des deutschen Steuerzahlers" }, - color: 0x00ff00, - }, - ], + color: 0x00ff00 + } + ] }); } @@ -172,7 +172,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const { item, template, attributes } = info; + const {item, template, attributes} = info; const effects = template.effects ?? []; @@ -202,14 +202,14 @@ export default class GegenstandCommand implements ApplicationCommand { const extraFields: (APIEmbedField | undefined)[] = [ template.onUse !== undefined - ? { name: "🔧 Benutzbar", value: "", inline: true } + ? {name: "🔧 Benutzbar", value: "", inline: true} : undefined, ...otherAttributes.map(attribute => ({ name: `${attribute.shortDisplay} ${attribute.displayName}`.trim(), value: "", - inline: true, - })), + inline: true + })) ]; await interaction.reply({ @@ -220,31 +220,31 @@ export default class GegenstandCommand implements ApplicationCommand { color: 0x00ff00, image: attachment ? { - url: "attachment://hero.gif", - width: 128, - } + url: "attachment://hero.gif", + width: 128 + } : undefined, fields: [ ...effects.map(value => ({ name: "⚡️ Effekt", value, - inline: true, + inline: true })), - ...extraFields.filter(e => e !== undefined), + ...extraFields.filter(e => e !== undefined) ], footer: { - text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim(), - }, - }, + text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim() + } + } ], files: attachment ? [ - { - name: "hero.gif", - attachment, - }, - ] - : [], + { + name: "hero.gif", + attachment + } + ] + : [] }); } @@ -261,12 +261,12 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const { item, template } = info; + const {item, template} = info; if (template.onUse === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht benutzt werden.", - ephemeral: true, + ephemeral: true }); return; } @@ -276,14 +276,14 @@ export default class GegenstandCommand implements ApplicationCommand { keepInInventory = await template.onUse( interaction as LootUseCommandInteraction, context, - item, + item ); } catch (error) { log.error(error, "Error while using item"); sentry.captureException(error); await interaction.reply({ content: "Beim Benutzen dieses Gegenstands ist ein Fehler aufgetreten.", - ephemeral: true, + ephemeral: true }); } @@ -302,7 +302,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (!item) { await interaction.reply({ content: "Diesen Gegensand hast du nicht.", - ephemeral: true, + ephemeral: true }); return; } @@ -311,14 +311,14 @@ export default class GegenstandCommand implements ApplicationCommand { if (!template) { await interaction.reply({ content: "Dieser Gegenstand ist unbekannt.", - ephemeral: true, + ephemeral: true }); return; } const attributes = await lootService.getLootAttributes(item.id); - return { item, template, attributes }; + return {item, template, attributes}; } async autocomplete(interaction: AutocompleteInteraction) { @@ -362,7 +362,7 @@ export default class GegenstandCommand implements ApplicationCommand { typeof emote === "string" ? `${emote} ${item.displayName} (${item.id})` : `${item.displayName} (${item.id})`, - value: String(item.id), + value: String(item.id) }); } @@ -381,20 +381,22 @@ export default class GegenstandCommand implements ApplicationCommand { if (!info) { return; } - const { item, template } = info; + const {item, template} = info; log.info(item); if (template.gameEquip === undefined) { await interaction.reply({ content: ` ${item.displayName} kann nicht ausgerüstet werden.`, - ephemeral: true, + ephemeral: true }); return; } const items = await getFightInventoryUnsorted(interaction.user.id); - if (items.filter(i => i.id == item.id)) { + console.log(item.id); + console.log(items); + if (items.filter(i => i.id === item.id).length !== 0) { await interaction.reply({ content: `Du hast ${item.displayName} schon ausgerüstet`, - ephemeral: true, + ephemeral: true }); return; } diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index 7c1b921c..d1a4d35c 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -8,19 +8,19 @@ import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandUserOption, - type User, + type User } from "discord.js"; -import type { BotContext } from "@/context.js"; -import type { ApplicationCommand } from "@/commands/command.js"; +import type {BotContext} from "@/context.js"; +import type {ApplicationCommand} from "@/commands/command.js"; import * as lootService from "@/service/loot.js"; -import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; -import { format } from "@/utils/stringUtils.js"; +import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; +import {format} from "@/utils/stringUtils.js"; import * as lootDataService from "@/service/lootData.js"; -import { LootAttributeKindId } from "@/service/lootData.js"; +import {LootAttributeKindId} from "@/service/lootData.js"; import log from "@log"; -import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; +import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; export default class InventarCommand implements ApplicationCommand { name = "inventar"; @@ -33,7 +33,7 @@ export default class InventarCommand implements ApplicationCommand { new SlashCommandUserOption() .setRequired(false) .setName("user") - .setDescription("Wem du tun willst"), + .setDescription("Wem du tun willst") ) .addStringOption( new SlashCommandStringOption() @@ -41,10 +41,10 @@ export default class InventarCommand implements ApplicationCommand { .setDescription("Anzeige") .setRequired(false) .addChoices( - { name: "Kampfausrüstung", value: "fightinventory" }, - { name: "Kurzfassung", value: "short" }, - { name: "Detailansicht", value: "long" }, - ), + {name: "Kampfausrüstung", value: "fightinventory"}, + {name: "Kurzfassung", value: "short"}, + {name: "Detailansicht", value: "long"} + ) ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -56,7 +56,7 @@ export default class InventarCommand implements ApplicationCommand { const contents = await lootService.getInventoryContents(user); if (contents.length === 0) { await interaction.reply({ - content: "Dein Inventar ist ✨leer✨", + content: "Dein Inventar ist ✨leer✨" }); return; } @@ -75,7 +75,7 @@ export default class InventarCommand implements ApplicationCommand { async #createShortEmbed( context: BotContext, interaction: CommandInteraction, - user: User, + user: User ) { const contents = await lootService.getInventoryContents(user); const groupedByLoot = Object.groupBy(contents, item => item.displayName); @@ -98,7 +98,7 @@ export default class InventarCommand implements ApplicationCommand { .join("\n"); const cuties = contents.filter(i => - lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET), + lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET) ).length; const message = /* mf2 */ ` @@ -119,10 +119,10 @@ export default class InventarCommand implements ApplicationCommand { title: `Inventar von ${user.displayName}`, description, footer: { - text: format(message, { cuties, count: contents.length - cuties }), - }, - }, - ], + text: format(message, {cuties, count: contents.length - cuties}) + } + } + ] }); } @@ -131,7 +131,7 @@ export default class InventarCommand implements ApplicationCommand { const contentsUnsorted = await lootService.getInventoryContents(user); const contents = contentsUnsorted.toSorted((a, b) => - b.createdAt.localeCompare(a.createdAt), + b.createdAt.localeCompare(a.createdAt) ); let lastPageIndex = Math.floor(contents.length / pageSize); @@ -156,12 +156,12 @@ export default class InventarCommand implements ApplicationCommand { return { name: `${lootDataService.getEmote(context.guild, item)} ${item.displayName}${rarity} ${shortAttributeList}`.trim(), value: "", - inline: false, + inline: false }; }), footer: { - text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}`, - }, + text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}` + } } satisfies APIEmbed; return { @@ -174,19 +174,19 @@ export default class InventarCommand implements ApplicationCommand { label: "<<", customId: "page-prev", disabled: pageIndex <= 0, - style: ButtonStyle.Secondary, + style: ButtonStyle.Secondary }, { type: ComponentType.Button, label: ">>", customId: "page-next", disabled: pageIndex >= lastPageIndex, - style: ButtonStyle.Secondary, - }, - ], - }, + style: ButtonStyle.Secondary + } + ] + } ], - embeds: [embed], + embeds: [embed] } as const; } @@ -195,12 +195,12 @@ export default class InventarCommand implements ApplicationCommand { const message = await interaction.reply({ ...buildMessageData(pageIndex), fetchReply: true, - tts: false, + tts: false }); const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 45_000, + time: 45_000 }); collector.on("collect", async i => { @@ -218,13 +218,13 @@ export default class InventarCommand implements ApplicationCommand { } await message.edit({ - ...buildMessageData(pageIndex), + ...buildMessageData(pageIndex) }); }); - collector.on("end", async () => { + collector.on("end", async() => { await message.edit({ - components: [], + components: [] }); }); } @@ -235,29 +235,29 @@ export default class InventarCommand implements ApplicationCommand { title: `Kampfausrüstung von ${user.displayName}`, description: "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", - thumbnail: user.avatarURL() ? { url: user.avatarURL()! } : undefined, + thumbnail: user.avatarURL() ? {url: user.avatarURL()!} : undefined, fields: [ - { name: "Waffe", value: fightinventory.weapon?.name ?? "Nix" }, - { name: "Rüstung", value: fightinventory.armor?.name ?? "Nackt" }, + {name: "Waffe", value: fightinventory.weapon?.itemInfo?.displayName ?? "Nix"}, + {name: "Rüstung", value: fightinventory.armor?.itemInfo?.displayName ?? "Nackt"}, ...fightinventory.items.map(item => { return { - name: item.name, + name: item.itemInfo?.displayName ?? "", value: "Hier sollten die buffs stehen", - inline: true, + inline: true }; }), - { name: "Buffs", value: "Nix" }, + {name: "Buffs", value: "Nix"} ], footer: { - text: `Lol ist noch nicht fertig`, - }, + text: `Lol ist noch nicht fertig` + } } satisfies APIEmbed; const embed = { components: [], embeds: [display], fetchReply: true, - tts: false, + tts: false } as const satisfies InteractionReplyOptions; const message = await interaction.reply(embed); diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 69b8f7f9..d6df2eb5 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,48 +1,55 @@ export const fightTemplates: { [name: string]: Equipable } = { ayran: { type: "item", - attackModifier: { min: 2, max: 3 }, + attackModifier: {min: 2, max: 3} }, oettinger: { type: "item", - attackModifier: { min: 1, max: 5 }, - defenceModifier: { min: -3, max: 0 }, + attackModifier: {min: 1, max: 5}, + defenceModifier: {min: -3, max: 0} }, thunfischshake: { type: "item", - attackModifier: { min: 3, max: 5 }, + attackModifier: {min: 3, max: 5} }, nachthemd: { type: "armor", - health: 50, - defence: { min: 2, max: 5 }, + defence: {min: 2, max: 5} }, eierwaermer: { type: "armor", - health: 30, - defence: { min: 3, max: 5 }, + defence: {min: 3, max: 5} }, dildo: { type: "weapon", - attack: { min: 3, max: 9 }, + attack: {min: 3, max: 9} }, messerblock: { type: "weapon", - attack: { min: 1, max: 9 }, - }, + attack: {min: 1, max: 9} + } }; -export const bossMap: { [name: string]: BaseEntity } = { +export const bossMap: { [name: string]: (Enemy) } = { gudrun: { - name: "Gudrun die Hexe", + name: "Gudrun", description: "", health: 150, baseDamage: 2, baseDefence: 0, - armor: fightTemplates.nachthemd as EquipableArmor, - weapon: fightTemplates.dildo as EquipableWeapon, - items: [], + enabled: true, + armor: { + name: "Nachthemd", + ...fightTemplates.nachthemd as EquipableArmor + }, + weapon: { + name: "Dildo", + ...fightTemplates.dildo as EquipableWeapon + }, + lossDescription: "", + winDescription: "", + items: [] }, deinchef: { @@ -51,41 +58,53 @@ export const bossMap: { [name: string]: BaseEntity } = { health: 120, baseDamage: 1, baseDefence: 1, - items: [], + enabled: false, + lossDescription: "", + winDescription: "", + items: [] }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", description: "", health: 120, + enabled: false, baseDamage: 1, baseDefence: 1, - items: [], + lossDescription: "", + winDescription: "", + items: [] }, rentner: { name: "Reeeeeeentner", description: "Runter von meinem Rasen, dein Auto muss da weg", + lossDescription: "", + winDescription: "", health: 200, baseDamage: 3, baseDefence: 5, - items: [], + enabled: false, + items: [] }, barkeeper: { - name: "Barkeeper aus Nürnia", + name: "Barkeeper von Nürnia", description: "Nach deiner Reise durch den Schrank durch kommst du nach Nürnia, wo dich ein freundlicher Barkeeper dich anlächelt " + "und dir ein Eimergroßes Fass Gettorade hinstellt. Deine nächste aufgabe ist es ihn im Wetttrinken zu besiegen", + lossDescription: "", + winDescription: "", health: 350, + enabled: false, baseDamage: 5, baseDefence: 5, - items: [], - }, + items: [] + } }; export const baseStats = { description: "", health: 80, baseDamage: 1, - baseDefence: 0, + baseDefence: 0 }; export type FightItemType = "weapon" | "armor" | "item"; @@ -114,13 +133,20 @@ export interface BaseEntity { description: string; baseDamage: number; baseDefence: number; - items: EquipableItem[]; - weapon?: EquipableWeapon; - armor?: EquipableArmor; + + items: (EquipableItem & { name: string }) []; + weapon?: EquipableWeapon & { name: string }; + armor?: EquipableArmor & { name: string }; //TODO permaBuffs?: undefined; } +export interface Enemy extends BaseEntity { + enabled: boolean; + winDescription: string; + lossDescription: string; +} + export class Entity { stats: BaseEntity; maxhealth: number; @@ -130,7 +156,7 @@ export class Entity { constructor(entity: BaseEntity) { this.stats = entity; - if (this.stats.armor) { + if (this.stats.armor?.health) { this.stats.health += this.stats.armor?.health; } this.maxhealth = this.stats.health; @@ -139,7 +165,7 @@ export class Entity { attack(enemy: Entity) { let rawDamage: number; rawDamage = this.stats.baseDamage; - if (this.stats.weapon) { + if (this.stats.weapon?.attack) { rawDamage += randomValue(this.stats.weapon.attack); } this.stats.items @@ -148,7 +174,7 @@ export class Entity { const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( - `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, + `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}` ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -157,7 +183,7 @@ export class Entity { defend() { let defence = this.stats.baseDefence; - if (this.stats.armor) { + if (this.stats.armor?.defence) { defence += randomValue(this.stats.armor.defence); } this.stats.items @@ -189,7 +215,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; + return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; } - return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; + return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; } diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index ea3b9da8..b82fe0eb 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -293,7 +293,8 @@ export interface EmoteUseTable extends AuditedTable { interface FightHistoryTable extends AuditedTable { id: GeneratedAlways; userid: Snowflake; - bossname: string; + result: boolean + bossName: string; firsttime: boolean; } diff --git a/src/storage/fighthistory.ts b/src/storage/fighthistory.ts new file mode 100644 index 00000000..c70dc9f5 --- /dev/null +++ b/src/storage/fighthistory.ts @@ -0,0 +1,36 @@ +import type {User} from "discord.js"; +import db from "@db"; +import {FightScene} from "@/service/fightData.js"; + + +export async function insertResult(userId: User["id"], boss: string, win: boolean, ctx = db()) { + const lastWins = await getWinsForBoss(userId, boss); + return await ctx + .insertInto("fighthistory") + .values({ + userid: userId, + result: win, + bossName: boss, + firsttime: lastWins.length == 0 && win + }) + .execute(); +} + +export async function getWinsForBoss(userId: User["id"], boss: string, ctx = db()) { + return await ctx + .selectFrom("fighthistory") + .where("userid", "=", userId) + .where("bossName", "=", boss) + .where("result", "=", true) + .selectAll() + .execute(); +} + +export async function getLastFight(userId: User["id"], ctx = db()) { + return await ctx + .selectFrom("fighthistory") + .where("userid", "=", userId) + .orderBy("createdAt desc") + .selectAll() + .executeTakeFirst(); +} diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts index ef6dd061..19b5e9da 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightinventory.ts @@ -1,16 +1,12 @@ -import type { User } from "discord.js"; +import type {User} from "discord.js"; import db from "@db"; -import { - Equipable, - EquipableArmor, - EquipableItem, - EquipableWeapon, - FightItemType, -} from "@/service/fightData.js"; -import type { LootId } from "@/storage/db/model.js"; +import {type Equipable} from "@/service/fightData.js"; +import type {LootId} from "@/storage/db/model.js"; import * as lootDataService from "@/service/lootData.js"; +import {type LootKindId, resolveLootTemplate} from "@/service/lootData.js"; import * as lootService from "@/service/loot.js"; -import { LootKindId, resolveLootTemplate } from "@/service/lootData.js"; +import {deleteLoot} from "@/storage/loot.js"; + export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { return await ctx @@ -24,30 +20,28 @@ export async function getFightInventoryEnriched(userId: User["id"], ctx = db()) const unsorted = await getFightInventoryUnsorted(userId, ctx); const enriched = []; for (const equip of unsorted) { + let itemInfo = await lootService.getUserLootById(userId, equip.lootId, ctx); enriched.push({ gameTemplate: await getGameTemplate( - (await lootService.getUserLootById(userId, equip.lootId, ctx))?.lootKindId, + itemInfo?.lootKindId ), - ...equip, + itemInfo: itemInfo }); } return { weapon: enriched .filter(value => value.gameTemplate?.type === "weapon") - .map(value => value.gameTemplate as EquipableWeapon) .shift(), armor: enriched .filter(value => value.gameTemplate?.type === "armor") - .map(value => value.gameTemplate as EquipableArmor) .shift(), items: enriched .filter(value => value.gameTemplate?.type === "item") - .map(value => value.gameTemplate as EquipableItem), }; } export async function getGameTemplate( - lootKindId: LootKindId | undefined, + lootKindId: LootKindId | undefined ): Promise { return lootKindId ? resolveLootTemplate(lootKindId)?.gameEquip : undefined; } @@ -61,6 +55,22 @@ export async function getItemsByType(userId: User["id"], fightItemType: string, .execute(); } +export async function removeItemsAfterFight(userId: User["id"], ctx = db()) { + await ctx.transaction().execute(async ctx => { + const items = await getItemsByType(userId, "item", ctx); + for (const item of items) { + await deleteLoot(item.lootId, ctx); + } + await ctx.deleteFrom("fightinventory") + .where("userid", "=", userId) + .where("equippedSlot", "=", "item") + .execute(); + + } + ); + +} + export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = db()) { const item = await lootService.getUserLootById(userId, lootId); const lootTemplate = lootDataService.resolveLootTemplate(item!.lootKindId)!; @@ -68,7 +78,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const maxItems = { weapon: 1, armor: 1, - item: 3, + item: 3 }; const unequippeditems: string[] = []; @@ -78,7 +88,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const unequipitem = await lootService.getUserLootById( userId, equippedStuff[i].lootId, - ctx, + ctx ); unequippeditems.push(unequipitem?.displayName ?? String(equippedStuff[i].lootId)); await ctx.deleteFrom("fightinventory").where("id", "=", equippedStuff[i].id).execute(); @@ -89,9 +99,9 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = .values({ userid: userId, lootId: lootId, - equippedSlot: type, + equippedSlot: type }) .execute(); - return { unequipped: unequippeditems, equipped: item }; + return {unequipped: unequippeditems, equipped: item}; }); } diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts index 6e02f6fb..2a8d0d04 100644 --- a/src/storage/migrations/11-fightsystem.ts +++ b/src/storage/migrations/11-fightsystem.ts @@ -16,6 +16,7 @@ export async function up(db: Kysely) { .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) .addColumn("userid", "text", c => c.notNull()) .addColumn("bossName", "text", c => c.notNull()) + .addColumn("result", "boolean", c => c.notNull()) .addColumn("firsttime", "boolean", c => c.notNull()) .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) From dbab44a8340474c47544a4769fe0577f3082f839 Mon Sep 17 00:00:00 2001 From: Feluin Date: Thu, 2 Jan 2025 18:19:43 +0100 Subject: [PATCH 16/26] added history and stuff --- src/commands/fight.ts | 97 +++++++++++++---------------- src/commands/gegenstand.ts | 112 +++++++++++++++++----------------- src/commands/inventar.ts | 84 ++++++++++++------------- src/service/fightData.ts | 46 +++++++------- src/storage/db/model.ts | 2 +- src/storage/fighthistory.ts | 7 +-- src/storage/fightinventory.ts | 58 ++++++++---------- 7 files changed, 192 insertions(+), 214 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index b7b3219a..4f14fee9 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -1,4 +1,4 @@ -import type {ApplicationCommand} from "@/commands/command.js"; +import type { ApplicationCommand } from "@/commands/command.js"; import { type APIEmbed, APIEmbedField, @@ -7,10 +7,10 @@ import { type CommandInteraction, type InteractionResponse, SlashCommandBuilder, - type User + type User, } from "discord.js"; -import type {BotContext} from "@/context.js"; -import type {JSONEncodable} from "@discordjs/util"; +import type { BotContext } from "@/context.js"; +import type { JSONEncodable } from "@discordjs/util"; import { type BaseEntity, baseStats, @@ -19,11 +19,11 @@ import { EquipableArmor, EquipableItem, EquipableWeapon, - type FightScene + type FightScene, } from "@/service/fightData.js"; -import {setTimeout} from "node:timers/promises"; -import {getFightInventoryEnriched, removeItemsAfterFight} from "@/storage/fightinventory.js"; -import {getLastFight, insertResult} from "@/storage/fighthistory.js"; +import { setTimeout } from "node:timers/promises"; +import { getFightInventoryEnriched, removeItemsAfterFight } from "@/storage/fightinventory.js"; +import { getLastFight, insertResult } from "@/storage/fighthistory.js"; async function getFighter(user: User): Promise { const userInventory = await getFightInventoryEnriched(user.id); @@ -33,19 +33,18 @@ async function getFighter(user: User): Promise { name: user.displayName, weapon: { name: userInventory.weapon?.itemInfo?.displayName ?? "Nichts", - ...userInventory.weapon?.gameTemplate as EquipableWeapon + ...(userInventory.weapon?.gameTemplate as EquipableWeapon), }, armor: { name: userInventory.armor?.itemInfo?.displayName ?? "Nichts", - ...userInventory.armor?.gameTemplate as EquipableArmor + ...(userInventory.armor?.gameTemplate as EquipableArmor), }, items: userInventory.items.map(value => { return { name: value.itemInfo?.displayName ?? "Error", - ...value.gameTemplate as EquipableItem + ...(value.gameTemplate as EquipableItem), }; - - }) + }), }; } @@ -62,13 +61,15 @@ export default class FightCommand implements ApplicationCommand { .setDescription("Boss") //switch to autocomplete when we reach 25 .addChoices( - Object.entries(bossMap).filter(boss => boss[1].enabled).map(boss => { - return { - name: boss[1].name, - value: boss[0] - }; - }) - ) + Object.entries(bossMap) + .filter(boss => boss[1].enabled) + .map(boss => { + return { + name: boss[1].name, + value: boss[0], + }; + }), + ), ); async handleInteraction(command: CommandInteraction, context: BotContext) { @@ -95,22 +96,13 @@ export default class FightCommand implements ApplicationCommand { const interactionResponse = await command.deferReply(); + const playerstats = await getFighter(command.user); - const - playerstats = await getFighter(command.user); - - - await - - fight(command - .user, playerstats, boss, {...bossMap[boss]}, - interactionResponse - ); - + await fight(command.user, playerstats, boss, { ...bossMap[boss] }, interactionResponse); } } -type result = "PLAYER" | "ENEMY" | undefined +type result = "PLAYER" | "ENEMY" | undefined; function checkWin(fightscene: FightScene): result { if (fightscene.player.stats.health < 0) { @@ -128,50 +120,49 @@ function renderEndScreen(fightscene: FightScene): APIEmbed | JSONEncodable value.name).join(" \n") - } + value: fightscene.player.stats.items.map(value => value.name).join(" \n"), + }, ]; if (result == "PLAYER") { return { title: `Mit viel Glück konnte ${fightscene.player.stats.name} ${fightscene.enemy.stats.name} besiegen `, - color: 0x57F287, + color: 0x57f287, description: fightscene.enemy.stats.description, - fields: fields + fields: fields, }; } if (result == "ENEMY") { return { title: `${fightscene.enemy.stats.name} hat ${fightscene.player.stats.name} gnadenlos vernichtet`, - color: 0xED4245, + color: 0xed4245, description: fightscene.enemy.stats.description, - fields: fields + fields: fields, }; } return { title: `Kampf zwischen ${fightscene.player.stats.name} und ${fightscene.enemy.stats.name}`, description: fightscene.enemy.stats.description, - fields: fields + fields: fields, }; } - -export async function fight(user: User, +export async function fight( + user: User, playerstats: BaseEntity, boss: string, enemystats: BaseEntity, - interactionResponse: - InteractionResponse> + interactionResponse: InteractionResponse>, ) { const enemy = new Entity(enemystats); const player = new Entity(playerstats); const scene: FightScene = { player: player, - enemy: enemy + enemy: enemy, }; while (checkWin(scene) === undefined) { player.itemtext = []; @@ -191,19 +182,17 @@ export async function fight(user: User, if (!value.afterFight) { continue; } - value.afterFight({player: enemy, enemy: player}); + value.afterFight({ player: enemy, enemy: player }); } - await interactionResponse.edit({embeds: [renderFightEmbedded(scene)]}); + await interactionResponse.edit({ embeds: [renderFightEmbedded(scene)] }); await setTimeout(200); } - await interactionResponse.edit({embeds: [renderEndScreen(scene)]}); + await interactionResponse.edit({ embeds: [renderEndScreen(scene)] }); //delete items await removeItemsAfterFight(user.id); // await insertResult(user.id, boss, checkWin(scene) == "PLAYER"); - - } function renderStats(player: Entity) { @@ -219,7 +208,7 @@ function renderStats(player: Entity) { 📚Items: ${player.itemtext.join("\n")} `, - inline: true + inline: true, }; } @@ -232,8 +221,8 @@ function renderFightEmbedded(fightscene: FightScene): JSONEncodable | renderStats(fightscene.enemy), { name: "Verlauf", - value: " " - } - ] + value: " ", + }, + ], }; } diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index 1f91cc4b..cc8d081d 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -7,24 +7,24 @@ import { type CommandInteraction, SlashCommandBuilder, SlashCommandStringOption, - SlashCommandSubcommandBuilder + SlashCommandSubcommandBuilder, } from "discord.js"; import * as sentry from "@sentry/bun"; -import type {BotContext} from "@/context.js"; -import type {ApplicationCommand} from "@/commands/command.js"; -import type {LootUseCommandInteraction} from "@/storage/loot.js"; +import type { BotContext } from "@/context.js"; +import type { ApplicationCommand } from "@/commands/command.js"; +import type { LootUseCommandInteraction } from "@/storage/loot.js"; import * as lootService from "@/service/loot.js"; import * as lootRoleService from "@/service/lootRoles.js"; -import {randomEntry} from "@/utils/arrayUtils.js"; -import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; +import { randomEntry } from "@/utils/arrayUtils.js"; +import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; import * as imageService from "@/service/image.js"; import * as lootDataService from "@/service/lootData.js"; -import {LootAttributeClassId, LootAttributeKindId, LootKindId} from "@/service/lootData.js"; +import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; import log from "@log"; -import {equipItembyLoot, getFightInventoryUnsorted} from "@/storage/fightinventory.js"; +import { equipItembyLoot, getFightInventoryUnsorted } from "@/storage/fightinventory.js"; export default class GegenstandCommand implements ApplicationCommand { name = "gegenstand"; @@ -36,7 +36,7 @@ export default class GegenstandCommand implements ApplicationCommand { .addSubcommand( new SlashCommandSubcommandBuilder() .setName("entsorgen") - .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes") + .setDescription("Gebe dem Wärter etwas Atommüll und etwas süßes"), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -47,8 +47,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Der Gegenstand, über den du Informationen haben möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -59,8 +59,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Die Sau, die du benutzen möchtest") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand( new SlashCommandSubcommandBuilder() @@ -71,8 +71,8 @@ export default class GegenstandCommand implements ApplicationCommand { .setRequired(true) .setName("item") .setDescription("Rüste dich für deinen nächsten Kampf") - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -104,33 +104,33 @@ export default class GegenstandCommand implements ApplicationCommand { { description: "Es ist kein Wärter im Dienst. Das Tor ist zu. Du rennst dagegen. Opfer.", - color: 0xff0000 - } - ] + color: 0xff0000, + }, + ], }); return; } const wasteContents = await lootService.getUserLootsByTypeId( interaction.user.id, - LootKindId.RADIOACTIVE_WASTE + LootKindId.RADIOACTIVE_WASTE, ); if (wasteContents.length === 0) { await interaction.reply({ - content: "Du hast keinen Atommüll, den du in die Grube werfen kannst." + content: "Du hast keinen Atommüll, den du in die Grube werfen kannst.", }); return; } const sweetContent = await lootService.getUserLootsWithAttribute( interaction.user.id, - LootAttributeKindId.SWEET + LootAttributeKindId.SWEET, ); if (sweetContent.length === 0) { await interaction.reply({ - content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst." + content: "Du hast keine süßen Sachen, mit denen du den Wärter bestechen kannst.", }); return; } @@ -142,7 +142,7 @@ export default class GegenstandCommand implements ApplicationCommand { const messages = [ `Du hast dem Wärter ${currentGuard} etwas Atommüll und etwas Süßes zum Naschen gegeben.`, `${currentGuard} hat sich über deinen Atommüll und die süßen Sachen gefreut.`, - `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.` + `${currentGuard} hat sich gerade die hübschen Vögel angeschaut. Du konntest unbemerkt ein Fass Atommüll an im vorbei rollen und hast ihm als Geschenk etwas süßes hinterlassen.`, ]; await interaction.reply({ @@ -151,11 +151,11 @@ export default class GegenstandCommand implements ApplicationCommand { title: "Atommüll entsorgt!", description: randomEntry(messages), footer: { - text: "Jetzt ist es das Problem des deutschen Steuerzahlers" + text: "Jetzt ist es das Problem des deutschen Steuerzahlers", }, - color: 0x00ff00 - } - ] + color: 0x00ff00, + }, + ], }); } @@ -172,7 +172,7 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const {item, template, attributes} = info; + const { item, template, attributes } = info; const effects = template.effects ?? []; @@ -202,14 +202,14 @@ export default class GegenstandCommand implements ApplicationCommand { const extraFields: (APIEmbedField | undefined)[] = [ template.onUse !== undefined - ? {name: "🔧 Benutzbar", value: "", inline: true} + ? { name: "🔧 Benutzbar", value: "", inline: true } : undefined, ...otherAttributes.map(attribute => ({ name: `${attribute.shortDisplay} ${attribute.displayName}`.trim(), value: "", - inline: true - })) + inline: true, + })), ]; await interaction.reply({ @@ -220,31 +220,31 @@ export default class GegenstandCommand implements ApplicationCommand { color: 0x00ff00, image: attachment ? { - url: "attachment://hero.gif", - width: 128 - } + url: "attachment://hero.gif", + width: 128, + } : undefined, fields: [ ...effects.map(value => ({ name: "⚡️ Effekt", value, - inline: true + inline: true, })), - ...extraFields.filter(e => e !== undefined) + ...extraFields.filter(e => e !== undefined), ], footer: { - text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim() - } - } + text: `${rarity.shortDisplay} ${rarity.displayName}\t\t\t\t\t\t${otherAttributes.map(a => a.shortDisplay).join("")}`.trim(), + }, + }, ], files: attachment ? [ - { - name: "hero.gif", - attachment - } - ] - : [] + { + name: "hero.gif", + attachment, + }, + ] + : [], }); } @@ -261,12 +261,12 @@ export default class GegenstandCommand implements ApplicationCommand { return; } - const {item, template} = info; + const { item, template } = info; if (template.onUse === undefined) { await interaction.reply({ content: "Dieser Gegenstand kann nicht benutzt werden.", - ephemeral: true + ephemeral: true, }); return; } @@ -276,14 +276,14 @@ export default class GegenstandCommand implements ApplicationCommand { keepInInventory = await template.onUse( interaction as LootUseCommandInteraction, context, - item + item, ); } catch (error) { log.error(error, "Error while using item"); sentry.captureException(error); await interaction.reply({ content: "Beim Benutzen dieses Gegenstands ist ein Fehler aufgetreten.", - ephemeral: true + ephemeral: true, }); } @@ -302,7 +302,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (!item) { await interaction.reply({ content: "Diesen Gegensand hast du nicht.", - ephemeral: true + ephemeral: true, }); return; } @@ -311,14 +311,14 @@ export default class GegenstandCommand implements ApplicationCommand { if (!template) { await interaction.reply({ content: "Dieser Gegenstand ist unbekannt.", - ephemeral: true + ephemeral: true, }); return; } const attributes = await lootService.getLootAttributes(item.id); - return {item, template, attributes}; + return { item, template, attributes }; } async autocomplete(interaction: AutocompleteInteraction) { @@ -362,7 +362,7 @@ export default class GegenstandCommand implements ApplicationCommand { typeof emote === "string" ? `${emote} ${item.displayName} (${item.id})` : `${item.displayName} (${item.id})`, - value: String(item.id) + value: String(item.id), }); } @@ -381,12 +381,12 @@ export default class GegenstandCommand implements ApplicationCommand { if (!info) { return; } - const {item, template} = info; + const { item, template } = info; log.info(item); if (template.gameEquip === undefined) { await interaction.reply({ content: ` ${item.displayName} kann nicht ausgerüstet werden.`, - ephemeral: true + ephemeral: true, }); return; } @@ -396,7 +396,7 @@ export default class GegenstandCommand implements ApplicationCommand { if (items.filter(i => i.id === item.id).length !== 0) { await interaction.reply({ content: `Du hast ${item.displayName} schon ausgerüstet`, - ephemeral: true + ephemeral: true, }); return; } diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index d1a4d35c..3c55bfbe 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -8,19 +8,19 @@ import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandUserOption, - type User + type User, } from "discord.js"; -import type {BotContext} from "@/context.js"; -import type {ApplicationCommand} from "@/commands/command.js"; +import type { BotContext } from "@/context.js"; +import type { ApplicationCommand } from "@/commands/command.js"; import * as lootService from "@/service/loot.js"; -import {ensureChatInputCommand} from "@/utils/interactionUtils.js"; -import {format} from "@/utils/stringUtils.js"; +import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; +import { format } from "@/utils/stringUtils.js"; import * as lootDataService from "@/service/lootData.js"; -import {LootAttributeKindId} from "@/service/lootData.js"; +import { LootAttributeKindId } from "@/service/lootData.js"; import log from "@log"; -import {getFightInventoryEnriched} from "@/storage/fightinventory.js"; +import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; export default class InventarCommand implements ApplicationCommand { name = "inventar"; @@ -33,7 +33,7 @@ export default class InventarCommand implements ApplicationCommand { new SlashCommandUserOption() .setRequired(false) .setName("user") - .setDescription("Wem du tun willst") + .setDescription("Wem du tun willst"), ) .addStringOption( new SlashCommandStringOption() @@ -41,10 +41,10 @@ export default class InventarCommand implements ApplicationCommand { .setDescription("Anzeige") .setRequired(false) .addChoices( - {name: "Kampfausrüstung", value: "fightinventory"}, - {name: "Kurzfassung", value: "short"}, - {name: "Detailansicht", value: "long"} - ) + { name: "Kampfausrüstung", value: "fightinventory" }, + { name: "Kurzfassung", value: "short" }, + { name: "Detailansicht", value: "long" }, + ), ); async handleInteraction(interaction: CommandInteraction, context: BotContext) { @@ -56,7 +56,7 @@ export default class InventarCommand implements ApplicationCommand { const contents = await lootService.getInventoryContents(user); if (contents.length === 0) { await interaction.reply({ - content: "Dein Inventar ist ✨leer✨" + content: "Dein Inventar ist ✨leer✨", }); return; } @@ -75,7 +75,7 @@ export default class InventarCommand implements ApplicationCommand { async #createShortEmbed( context: BotContext, interaction: CommandInteraction, - user: User + user: User, ) { const contents = await lootService.getInventoryContents(user); const groupedByLoot = Object.groupBy(contents, item => item.displayName); @@ -98,7 +98,7 @@ export default class InventarCommand implements ApplicationCommand { .join("\n"); const cuties = contents.filter(i => - lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET) + lootDataService.itemHasAttribute(i.attributes, LootAttributeKindId.SWEET), ).length; const message = /* mf2 */ ` @@ -119,10 +119,10 @@ export default class InventarCommand implements ApplicationCommand { title: `Inventar von ${user.displayName}`, description, footer: { - text: format(message, {cuties, count: contents.length - cuties}) - } - } - ] + text: format(message, { cuties, count: contents.length - cuties }), + }, + }, + ], }); } @@ -131,7 +131,7 @@ export default class InventarCommand implements ApplicationCommand { const contentsUnsorted = await lootService.getInventoryContents(user); const contents = contentsUnsorted.toSorted((a, b) => - b.createdAt.localeCompare(a.createdAt) + b.createdAt.localeCompare(a.createdAt), ); let lastPageIndex = Math.floor(contents.length / pageSize); @@ -156,12 +156,12 @@ export default class InventarCommand implements ApplicationCommand { return { name: `${lootDataService.getEmote(context.guild, item)} ${item.displayName}${rarity} ${shortAttributeList}`.trim(), value: "", - inline: false + inline: false, }; }), footer: { - text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}` - } + text: `Seite ${pageIndex + 1} von ${lastPageIndex + 1}`, + }, } satisfies APIEmbed; return { @@ -174,19 +174,19 @@ export default class InventarCommand implements ApplicationCommand { label: "<<", customId: "page-prev", disabled: pageIndex <= 0, - style: ButtonStyle.Secondary + style: ButtonStyle.Secondary, }, { type: ComponentType.Button, label: ">>", customId: "page-next", disabled: pageIndex >= lastPageIndex, - style: ButtonStyle.Secondary - } - ] - } + style: ButtonStyle.Secondary, + }, + ], + }, ], - embeds: [embed] + embeds: [embed], } as const; } @@ -195,12 +195,12 @@ export default class InventarCommand implements ApplicationCommand { const message = await interaction.reply({ ...buildMessageData(pageIndex), fetchReply: true, - tts: false + tts: false, }); const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 45_000 + time: 45_000, }); collector.on("collect", async i => { @@ -218,13 +218,13 @@ export default class InventarCommand implements ApplicationCommand { } await message.edit({ - ...buildMessageData(pageIndex) + ...buildMessageData(pageIndex), }); }); - collector.on("end", async() => { + collector.on("end", async () => { await message.edit({ - components: [] + components: [], }); }); } @@ -235,29 +235,29 @@ export default class InventarCommand implements ApplicationCommand { title: `Kampfausrüstung von ${user.displayName}`, description: "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", - thumbnail: user.avatarURL() ? {url: user.avatarURL()!} : undefined, + thumbnail: user.avatarURL() ? { url: user.avatarURL()! } : undefined, fields: [ - {name: "Waffe", value: fightinventory.weapon?.itemInfo?.displayName ?? "Nix"}, - {name: "Rüstung", value: fightinventory.armor?.itemInfo?.displayName ?? "Nackt"}, + { name: "Waffe", value: fightinventory.weapon?.itemInfo?.displayName ?? "Nix" }, + { name: "Rüstung", value: fightinventory.armor?.itemInfo?.displayName ?? "Nackt" }, ...fightinventory.items.map(item => { return { name: item.itemInfo?.displayName ?? "", value: "Hier sollten die buffs stehen", - inline: true + inline: true, }; }), - {name: "Buffs", value: "Nix"} + { name: "Buffs", value: "Nix" }, ], footer: { - text: `Lol ist noch nicht fertig` - } + text: `Lol ist noch nicht fertig`, + }, } satisfies APIEmbed; const embed = { components: [], embeds: [display], fetchReply: true, - tts: false + tts: false, } as const satisfies InteractionReplyOptions; const message = await interaction.reply(embed); diff --git a/src/service/fightData.ts b/src/service/fightData.ts index d6df2eb5..d4c70809 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,37 +1,37 @@ export const fightTemplates: { [name: string]: Equipable } = { ayran: { type: "item", - attackModifier: {min: 2, max: 3} + attackModifier: { min: 2, max: 3 }, }, oettinger: { type: "item", - attackModifier: {min: 1, max: 5}, - defenceModifier: {min: -3, max: 0} + attackModifier: { min: 1, max: 5 }, + defenceModifier: { min: -3, max: 0 }, }, thunfischshake: { type: "item", - attackModifier: {min: 3, max: 5} + attackModifier: { min: 3, max: 5 }, }, nachthemd: { type: "armor", health: 50, - defence: {min: 2, max: 5} + defence: { min: 2, max: 5 }, }, eierwaermer: { type: "armor", health: 30, - defence: {min: 3, max: 5} + defence: { min: 3, max: 5 }, }, dildo: { type: "weapon", - attack: {min: 3, max: 9} + attack: { min: 3, max: 9 }, }, messerblock: { type: "weapon", - attack: {min: 1, max: 9} - } + attack: { min: 1, max: 9 }, + }, }; -export const bossMap: { [name: string]: (Enemy) } = { +export const bossMap: { [name: string]: Enemy } = { gudrun: { name: "Gudrun", description: "", @@ -41,15 +41,15 @@ export const bossMap: { [name: string]: (Enemy) } = { enabled: true, armor: { name: "Nachthemd", - ...fightTemplates.nachthemd as EquipableArmor + ...(fightTemplates.nachthemd as EquipableArmor), }, weapon: { name: "Dildo", - ...fightTemplates.dildo as EquipableWeapon + ...(fightTemplates.dildo as EquipableWeapon), }, lossDescription: "", winDescription: "", - items: [] + items: [], }, deinchef: { @@ -61,7 +61,7 @@ export const bossMap: { [name: string]: (Enemy) } = { enabled: false, lossDescription: "", winDescription: "", - items: [] + items: [], }, schutzheiliger: { name: "Schutzheiliger der Matjesverkäufer", @@ -72,7 +72,7 @@ export const bossMap: { [name: string]: (Enemy) } = { baseDefence: 1, lossDescription: "", winDescription: "", - items: [] + items: [], }, rentner: { name: "Reeeeeeentner", @@ -83,7 +83,7 @@ export const bossMap: { [name: string]: (Enemy) } = { baseDamage: 3, baseDefence: 5, enabled: false, - items: [] + items: [], }, barkeeper: { name: "Barkeeper von Nürnia", @@ -96,15 +96,15 @@ export const bossMap: { [name: string]: (Enemy) } = { enabled: false, baseDamage: 5, baseDefence: 5, - items: [] - } + items: [], + }, }; export const baseStats = { description: "", health: 80, baseDamage: 1, - baseDefence: 0 + baseDefence: 0, }; export type FightItemType = "weapon" | "armor" | "item"; @@ -134,7 +134,7 @@ export interface BaseEntity { baseDamage: number; baseDefence: number; - items: (EquipableItem & { name: string }) []; + items: (EquipableItem & { name: string })[]; weapon?: EquipableWeapon & { name: string }; armor?: EquipableArmor & { name: string }; //TODO @@ -174,7 +174,7 @@ export class Entity { const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( - `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}` + `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, ); enemy.stats.health -= result.damage; this.lastattack = result.rawDamage; @@ -215,7 +215,7 @@ function randomValue(range: Range) { function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { - return {rawDamage: rawDamage, damage: 0, mitigated: rawDamage}; + return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; } - return {rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence}; + return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; } diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index b82fe0eb..9700c0cb 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -293,7 +293,7 @@ export interface EmoteUseTable extends AuditedTable { interface FightHistoryTable extends AuditedTable { id: GeneratedAlways; userid: Snowflake; - result: boolean + result: boolean; bossName: string; firsttime: boolean; } diff --git a/src/storage/fighthistory.ts b/src/storage/fighthistory.ts index c70dc9f5..6ad8c1c1 100644 --- a/src/storage/fighthistory.ts +++ b/src/storage/fighthistory.ts @@ -1,7 +1,6 @@ -import type {User} from "discord.js"; +import type { User } from "discord.js"; import db from "@db"; -import {FightScene} from "@/service/fightData.js"; - +import { FightScene } from "@/service/fightData.js"; export async function insertResult(userId: User["id"], boss: string, win: boolean, ctx = db()) { const lastWins = await getWinsForBoss(userId, boss); @@ -11,7 +10,7 @@ export async function insertResult(userId: User["id"], boss: string, win: boolea userid: userId, result: win, bossName: boss, - firsttime: lastWins.length == 0 && win + firsttime: lastWins.length == 0 && win, }) .execute(); } diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts index 19b5e9da..f6ff2d05 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightinventory.ts @@ -1,12 +1,11 @@ -import type {User} from "discord.js"; +import type { User } from "discord.js"; import db from "@db"; -import {type Equipable} from "@/service/fightData.js"; -import type {LootId} from "@/storage/db/model.js"; +import { type Equipable } from "@/service/fightData.js"; +import type { LootId } from "@/storage/db/model.js"; import * as lootDataService from "@/service/lootData.js"; -import {type LootKindId, resolveLootTemplate} from "@/service/lootData.js"; +import { type LootKindId, resolveLootTemplate } from "@/service/lootData.js"; import * as lootService from "@/service/loot.js"; -import {deleteLoot} from "@/storage/loot.js"; - +import { deleteLoot } from "@/storage/loot.js"; export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { return await ctx @@ -22,26 +21,19 @@ export async function getFightInventoryEnriched(userId: User["id"], ctx = db()) for (const equip of unsorted) { let itemInfo = await lootService.getUserLootById(userId, equip.lootId, ctx); enriched.push({ - gameTemplate: await getGameTemplate( - itemInfo?.lootKindId - ), - itemInfo: itemInfo + gameTemplate: await getGameTemplate(itemInfo?.lootKindId), + itemInfo: itemInfo, }); } return { - weapon: enriched - .filter(value => value.gameTemplate?.type === "weapon") - .shift(), - armor: enriched - .filter(value => value.gameTemplate?.type === "armor") - .shift(), - items: enriched - .filter(value => value.gameTemplate?.type === "item") + weapon: enriched.filter(value => value.gameTemplate?.type === "weapon").shift(), + armor: enriched.filter(value => value.gameTemplate?.type === "armor").shift(), + items: enriched.filter(value => value.gameTemplate?.type === "item"), }; } export async function getGameTemplate( - lootKindId: LootKindId | undefined + lootKindId: LootKindId | undefined, ): Promise { return lootKindId ? resolveLootTemplate(lootKindId)?.gameEquip : undefined; } @@ -57,18 +49,16 @@ export async function getItemsByType(userId: User["id"], fightItemType: string, export async function removeItemsAfterFight(userId: User["id"], ctx = db()) { await ctx.transaction().execute(async ctx => { - const items = await getItemsByType(userId, "item", ctx); - for (const item of items) { - await deleteLoot(item.lootId, ctx); - } - await ctx.deleteFrom("fightinventory") - .where("userid", "=", userId) - .where("equippedSlot", "=", "item") - .execute(); - + const items = await getItemsByType(userId, "item", ctx); + for (const item of items) { + await deleteLoot(item.lootId, ctx); } - ); - + await ctx + .deleteFrom("fightinventory") + .where("userid", "=", userId) + .where("equippedSlot", "=", "item") + .execute(); + }); } export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = db()) { @@ -78,7 +68,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const maxItems = { weapon: 1, armor: 1, - item: 3 + item: 3, }; const unequippeditems: string[] = []; @@ -88,7 +78,7 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = const unequipitem = await lootService.getUserLootById( userId, equippedStuff[i].lootId, - ctx + ctx, ); unequippeditems.push(unequipitem?.displayName ?? String(equippedStuff[i].lootId)); await ctx.deleteFrom("fightinventory").where("id", "=", equippedStuff[i].id).execute(); @@ -99,9 +89,9 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = .values({ userid: userId, lootId: lootId, - equippedSlot: type + equippedSlot: type, }) .execute(); - return {unequipped: unequippeditems, equipped: item}; + return { unequipped: unequippeditems, equipped: item }; }); } From 679b41b634b565b8709e6b861a8b17913a9cc52b Mon Sep 17 00:00:00 2001 From: Feluin Date: Fri, 3 Jan 2025 16:39:36 +0100 Subject: [PATCH 17/26] fixed lint and reverted multiple drops --- src/commands/fight.ts | 14 +++++++------- src/commands/gegenstand.ts | 7 ++----- src/commands/inventar.ts | 5 +++-- src/service/fightData.ts | 18 ++++++++++-------- src/service/lootDrop.ts | 20 -------------------- src/storage/fighthistory.ts | 2 +- src/storage/fightinventory.ts | 28 +++++++++++++++------------- 7 files changed, 38 insertions(+), 56 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 4f14fee9..799d1e78 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -16,9 +16,9 @@ import { baseStats, bossMap, Entity, - EquipableArmor, - EquipableItem, - EquipableWeapon, + type EquipableArmor, + type EquipableItem, + type EquipableWeapon, type FightScene, } from "@/service/fightData.js"; import { setTimeout } from "node:timers/promises"; @@ -75,7 +75,7 @@ export default class FightCommand implements ApplicationCommand { async handleInteraction(command: CommandInteraction, context: BotContext) { const boss = command.options.get("boss", true).value as string; - let lastFight = await getLastFight(command.user.id); + const lastFight = await getLastFight(command.user.id); // if (lastFight !== undefined && (new Date(lastFight?.createdAt).getTime() > new Date().getTime() - 1000 * 60 * 60 * 24 * 5)) { // await command.reply({ @@ -127,7 +127,7 @@ function renderEndScreen(fightscene: FightScene): APIEmbed | JSONEncodable value.name).join(" \n"), }, ]; - if (result == "PLAYER") { + if (result === "PLAYER") { return { title: `Mit viel Glück konnte ${fightscene.player.stats.name} ${fightscene.enemy.stats.name} besiegen `, color: 0x57f287, @@ -135,7 +135,7 @@ function renderEndScreen(fightscene: FightScene): APIEmbed | JSONEncodable i.id === item.id).length !== 0) { await interaction.reply({ content: `Du hast ${item.displayName} schon ausgerüstet`, @@ -400,10 +398,9 @@ export default class GegenstandCommand implements ApplicationCommand { }); return; } - const result = await equipItembyLoot(interaction.user.id, item.id); - log.info(result); + const result = await equipItembyLoot(interaction.user.id, item, template.gameEquip.type); const message = - result.unequipped.length == 0 + result.unequipped.length === 0 ? `Du hast ${result.equipped?.displayName} ausgerüstet` : `Du hast ${result.unequipped.join(", ")} abgelegt und dafür ${result.equipped?.displayName} ausgerüstet`; await interaction.reply(message); diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index 3c55bfbe..7c63e031 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -231,11 +231,12 @@ export default class InventarCommand implements ApplicationCommand { async #createFightEmbed(context: BotContext, interaction: CommandInteraction, user: User) { const fightinventory = await getFightInventoryEnriched(user.id); + const avatarURL = user.avatarURL(); const display = { title: `Kampfausrüstung von ${user.displayName}`, description: "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", - thumbnail: user.avatarURL() ? { url: user.avatarURL()! } : undefined, + thumbnail: avatarURL ? { url: avatarURL } : undefined, fields: [ { name: "Waffe", value: fightinventory.weapon?.itemInfo?.displayName ?? "Nix" }, { name: "Rüstung", value: fightinventory.armor?.itemInfo?.displayName ?? "Nackt" }, @@ -249,7 +250,7 @@ export default class InventarCommand implements ApplicationCommand { { name: "Buffs", value: "Nix" }, ], footer: { - text: `Lol ist noch nicht fertig`, + text: "Lol ist noch nicht fertig", }, } satisfies APIEmbed; diff --git a/src/service/fightData.ts b/src/service/fightData.ts index d4c70809..405739ab 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -168,9 +168,11 @@ export class Entity { if (this.stats.weapon?.attack) { rawDamage += randomValue(this.stats.weapon.attack); } - this.stats.items - .filter(value => value.attackModifier) - .forEach(value => (rawDamage += randomValue(value.attackModifier!))); + for (const value1 of this.stats.items) { + if (value1.attackModifier) { + rawDamage += randomValue(value1.attackModifier); + } + } const defence = enemy.defend(); const result = calcDamage(rawDamage, defence); console.log( @@ -186,9 +188,11 @@ export class Entity { if (this.stats.armor?.defence) { defence += randomValue(this.stats.armor.defence); } - this.stats.items - .filter(value => value.defenceModifier) - .forEach(value => (defence += randomValue(value.defenceModifier!))); + for (const item of this.stats.items) { + if (item.defenceModifier) { + defence += randomValue(item.defenceModifier); + } + } this.lastdefence = defence; return defence; } @@ -199,8 +203,6 @@ export interface Range { max: number; } -type PermaBuff = {}; - export interface EquipableItem { type: "item"; attackModifier?: Range; diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index 1fc52401..cb4b1a29 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -152,26 +152,6 @@ export async function postLootDrop( return; } - for (let i = 0; i < 50; i++) { - const defaultWeights = lootTemplates.map(t => t.weight); - const { messages, weights } = await getDropWeightAdjustments( - interaction.user, - defaultWeights, - ); - - const rarityWeights = lootAttributeTemplates.map(a => a.initialDropWeight ?? 0); - const initialAttribute = randomEntryWeighted(lootAttributeTemplates, rarityWeights); - - const template = randomEntryWeighted(lootTemplates, weights); - const claimedLoot = await lootService.createLoot( - template, - interaction.user, - message, - "drop", - predecessorLootId ?? null, - initialAttribute, - ); - } const defaultWeights = lootTemplates.map(t => t.weight); const { messages, weights } = await getDropWeightAdjustments(interaction.user, defaultWeights); diff --git a/src/storage/fighthistory.ts b/src/storage/fighthistory.ts index 6ad8c1c1..892db891 100644 --- a/src/storage/fighthistory.ts +++ b/src/storage/fighthistory.ts @@ -10,7 +10,7 @@ export async function insertResult(userId: User["id"], boss: string, win: boolea userid: userId, result: win, bossName: boss, - firsttime: lastWins.length == 0 && win, + firsttime: lastWins.length === 0 && win, }) .execute(); } diff --git a/src/storage/fightinventory.ts b/src/storage/fightinventory.ts index f6ff2d05..c0a3cbcd 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightinventory.ts @@ -1,7 +1,7 @@ import type { User } from "discord.js"; import db from "@db"; -import { type Equipable } from "@/service/fightData.js"; -import type { LootId } from "@/storage/db/model.js"; +import type { Equipable, FightItemType } from "@/service/fightData.js"; +import type { Loot, LootId } from "@/storage/db/model.js"; import * as lootDataService from "@/service/lootData.js"; import { type LootKindId, resolveLootTemplate } from "@/service/lootData.js"; import * as lootService from "@/service/loot.js"; @@ -19,7 +19,7 @@ export async function getFightInventoryEnriched(userId: User["id"], ctx = db()) const unsorted = await getFightInventoryUnsorted(userId, ctx); const enriched = []; for (const equip of unsorted) { - let itemInfo = await lootService.getUserLootById(userId, equip.lootId, ctx); + const itemInfo = await lootService.getUserLootById(userId, equip.lootId, ctx); enriched.push({ gameTemplate: await getGameTemplate(itemInfo?.lootKindId), itemInfo: itemInfo, @@ -61,20 +61,22 @@ export async function removeItemsAfterFight(userId: User["id"], ctx = db()) { }); } -export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = db()) { - const item = await lootService.getUserLootById(userId, lootId); - const lootTemplate = lootDataService.resolveLootTemplate(item!.lootKindId)!; - const type = lootTemplate.gameEquip!.type; +export async function equipItembyLoot( + userId: User["id"], + loot: Loot, + itemType: FightItemType, + ctx = db(), +) { const maxItems = { weapon: 1, armor: 1, item: 3, }; - const unequippeditems: string[] = []; + return await ctx.transaction().execute(async ctx => { - const equippedStuff = await getItemsByType(userId, type, ctx); - for (let i = 0; i <= equippedStuff.length - maxItems[type]; i++) { + const equippedStuff = await getItemsByType(userId, itemType, ctx); + for (let i = 0; i <= equippedStuff.length - maxItems[itemType]; i++) { const unequipitem = await lootService.getUserLootById( userId, equippedStuff[i].lootId, @@ -88,10 +90,10 @@ export async function equipItembyLoot(userId: User["id"], lootId: LootId, ctx = .insertInto("fightinventory") .values({ userid: userId, - lootId: lootId, - equippedSlot: type, + lootId: loot.id, + equippedSlot: itemType, }) .execute(); - return { unequipped: unequippeditems, equipped: item }; + return { unequipped: unequippeditems, equipped: loot }; }); } From 3e8a0d0ed53a727929c8accd104628a0bebd6198 Mon Sep 17 00:00:00 2001 From: Feluin Date: Fri, 3 Jan 2025 16:43:17 +0100 Subject: [PATCH 18/26] fixed copy paste error --- src/storage/db/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index 9700c0cb..51319959 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -40,7 +40,7 @@ export type Birthday = Selectable; export interface BirthdayTable extends AuditedTable { id: GeneratedAlways; - userid: Snowflake; + userId: Snowflake; month: OneBasedMonth; day: number; } From 8eafebe5ac51c6c1f7ca56ae22e53adbc507241f Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 14:43:27 +0100 Subject: [PATCH 19/26] Fix casing of fightHistory + fightInventory --- src/commands/fight.ts | 4 ++-- src/commands/gegenstand.ts | 2 +- src/commands/inventar.ts | 14 +++++++------- src/storage/db/model.ts | 4 ++-- src/storage/{fighthistory.ts => fightHistory.ts} | 6 +++--- .../{fightinventory.ts => fightInventory.ts} | 10 +++++----- src/storage/migrations/11-fightsystem.ts | 8 +++++--- 7 files changed, 25 insertions(+), 23 deletions(-) rename src/storage/{fighthistory.ts => fightHistory.ts} (89%) rename src/storage/{fightinventory.ts => fightInventory.ts} (93%) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 799d1e78..7a96d7c1 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -22,8 +22,8 @@ import { type FightScene, } from "@/service/fightData.js"; import { setTimeout } from "node:timers/promises"; -import { getFightInventoryEnriched, removeItemsAfterFight } from "@/storage/fightinventory.js"; -import { getLastFight, insertResult } from "@/storage/fighthistory.js"; +import { getFightInventoryEnriched, removeItemsAfterFight } from "@/storage/fightInventory.js"; +import { getLastFight, insertResult } from "@/storage/fightHistory.js"; async function getFighter(user: User): Promise { const userInventory = await getFightInventoryEnriched(user.id); diff --git a/src/commands/gegenstand.ts b/src/commands/gegenstand.ts index 4b160569..51b4f2a8 100644 --- a/src/commands/gegenstand.ts +++ b/src/commands/gegenstand.ts @@ -24,7 +24,7 @@ import * as lootDataService from "@/service/lootData.js"; import { LootAttributeClassId, LootAttributeKindId, LootKindId } from "@/service/lootData.js"; import log from "@log"; -import { equipItembyLoot, getFightInventoryUnsorted } from "@/storage/fightinventory.js"; +import { equipItembyLoot, getFightInventoryUnsorted } from "@/storage/fightInventory.js"; export default class GegenstandCommand implements ApplicationCommand { name = "gegenstand"; diff --git a/src/commands/inventar.ts b/src/commands/inventar.ts index 2994c304..6161d432 100644 --- a/src/commands/inventar.ts +++ b/src/commands/inventar.ts @@ -18,7 +18,7 @@ import * as lootDataService from "@/service/lootData.js"; import { LootAttributeKindId } from "@/service/lootData.js"; import log from "@log"; -import { getFightInventoryEnriched } from "@/storage/fightinventory.js"; +import { getFightInventoryEnriched } from "@/storage/fightInventory.js"; export default class InventarCommand implements ApplicationCommand { name = "inventar"; @@ -39,7 +39,7 @@ export default class InventarCommand implements ApplicationCommand { .setDescription("Anzeige") .setRequired(false) .addChoices( - { name: "Kampfausrüstung", value: "fightinventory" }, + { name: "Kampfausrüstung", value: "fightInventory" }, { name: "Komplett", value: "all" }, ), ); @@ -59,7 +59,7 @@ export default class InventarCommand implements ApplicationCommand { } switch (type) { - case "fightinventory": + case "fightInventory": return await this.#createFightEmbed(context, interaction, user); case "all": return await this.#createLongEmbed(context, interaction, user); @@ -172,7 +172,7 @@ export default class InventarCommand implements ApplicationCommand { } async #createFightEmbed(context: BotContext, interaction: CommandInteraction, user: User) { - const fightinventory = await getFightInventoryEnriched(user.id); + const fightInventory = await getFightInventoryEnriched(user.id); const avatarURL = user.avatarURL(); const display = { title: `Kampfausrüstung von ${user.displayName}`, @@ -180,9 +180,9 @@ export default class InventarCommand implements ApplicationCommand { "Du kannst maximal eine Rüstung, eine Waffe und drei Items tragen. Wenn du kämpfst, setzt du die Items ein und verlierst diese, egal ob du gewinnst oder verlierst.", thumbnail: avatarURL ? { url: avatarURL } : undefined, fields: [ - { name: "Waffe", value: fightinventory.weapon?.itemInfo?.displayName ?? "Nix" }, - { name: "Rüstung", value: fightinventory.armor?.itemInfo?.displayName ?? "Nackt" }, - ...fightinventory.items.map(item => { + { name: "Waffe", value: fightInventory.weapon?.itemInfo?.displayName ?? "Nix" }, + { name: "Rüstung", value: fightInventory.armor?.itemInfo?.displayName ?? "Nackt" }, + ...fightInventory.items.map(item => { return { name: item.itemInfo?.displayName ?? "", value: "Hier sollten die buffs stehen", diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index a0cd1064..f3c21278 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -24,8 +24,8 @@ export interface Database { lootAttribute: LootAttributeTable; emote: EmoteTable; emoteUse: EmoteUseTable; - fighthistory: FightHistoryTable; - fightinventory: FightInventoryTable; + fightHistory: FightHistoryTable; + fightInventory: FightInventoryTable; } export type OneBasedMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; diff --git a/src/storage/fighthistory.ts b/src/storage/fightHistory.ts similarity index 89% rename from src/storage/fighthistory.ts rename to src/storage/fightHistory.ts index 892db891..2cf33c64 100644 --- a/src/storage/fighthistory.ts +++ b/src/storage/fightHistory.ts @@ -5,7 +5,7 @@ import { FightScene } from "@/service/fightData.js"; export async function insertResult(userId: User["id"], boss: string, win: boolean, ctx = db()) { const lastWins = await getWinsForBoss(userId, boss); return await ctx - .insertInto("fighthistory") + .insertInto("fightHistory") .values({ userid: userId, result: win, @@ -17,7 +17,7 @@ export async function insertResult(userId: User["id"], boss: string, win: boolea export async function getWinsForBoss(userId: User["id"], boss: string, ctx = db()) { return await ctx - .selectFrom("fighthistory") + .selectFrom("fightHistory") .where("userid", "=", userId) .where("bossName", "=", boss) .where("result", "=", true) @@ -27,7 +27,7 @@ export async function getWinsForBoss(userId: User["id"], boss: string, ctx = db( export async function getLastFight(userId: User["id"], ctx = db()) { return await ctx - .selectFrom("fighthistory") + .selectFrom("fightHistory") .where("userid", "=", userId) .orderBy("createdAt desc") .selectAll() diff --git a/src/storage/fightinventory.ts b/src/storage/fightInventory.ts similarity index 93% rename from src/storage/fightinventory.ts rename to src/storage/fightInventory.ts index c0a3cbcd..4a3d082f 100644 --- a/src/storage/fightinventory.ts +++ b/src/storage/fightInventory.ts @@ -9,7 +9,7 @@ import { deleteLoot } from "@/storage/loot.js"; export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { return await ctx - .selectFrom("fightinventory") + .selectFrom("fightInventory") .where("userid", "=", userId) .selectAll() .execute(); @@ -40,7 +40,7 @@ export async function getGameTemplate( export async function getItemsByType(userId: User["id"], fightItemType: string, ctx = db()) { return await ctx - .selectFrom("fightinventory") + .selectFrom("fightInventory") .where("userid", "=", userId) .where("equippedSlot", "=", fightItemType) .selectAll() @@ -54,7 +54,7 @@ export async function removeItemsAfterFight(userId: User["id"], ctx = db()) { await deleteLoot(item.lootId, ctx); } await ctx - .deleteFrom("fightinventory") + .deleteFrom("fightInventory") .where("userid", "=", userId) .where("equippedSlot", "=", "item") .execute(); @@ -83,11 +83,11 @@ export async function equipItembyLoot( ctx, ); unequippeditems.push(unequipitem?.displayName ?? String(equippedStuff[i].lootId)); - await ctx.deleteFrom("fightinventory").where("id", "=", equippedStuff[i].id).execute(); + await ctx.deleteFrom("fightInventory").where("id", "=", equippedStuff[i].id).execute(); } await ctx - .insertInto("fightinventory") + .insertInto("fightInventory") .values({ userid: userId, lootId: loot.id, diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts index 2a8d0d04..15c42761 100644 --- a/src/storage/migrations/11-fightsystem.ts +++ b/src/storage/migrations/11-fightsystem.ts @@ -3,15 +3,16 @@ import { createUpdatedAtTrigger } from "@/storage/migrations/10-loot-attributes. export async function up(db: Kysely) { await db.schema - .createTable("fightinventory") + .createTable("fightInventory") .ifNotExists() .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) .addColumn("userid", "text") .addColumn("lootId", "integer", c => c.references("loot.id")) .addColumn("equippedSlot", "text") .execute(); + await db.schema - .createTable("fighthistory") + .createTable("fightHistory") .ifNotExists() .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) .addColumn("userid", "text", c => c.notNull()) @@ -21,7 +22,8 @@ export async function up(db: Kysely) { .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .execute(); - await createUpdatedAtTrigger(db, "fighthistory"); + + await createUpdatedAtTrigger(db, "fightHistory"); } export async function down(_db: Kysely) { From a89dbc3d1f69b8fd37e7e0ee5c5690bdec493644 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 14:53:57 +0100 Subject: [PATCH 20/26] Move to service --- src/service/fightData.ts | 11 ++--------- src/service/random.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 src/service/random.ts diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 405739ab..8a471e56 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -1,3 +1,5 @@ +import { randomValue, type Range } from "@/service/random.js"; + export const fightTemplates: { [name: string]: Equipable } = { ayran: { type: "item", @@ -198,11 +200,6 @@ export class Entity { } } -export interface Range { - min: number; - max: number; -} - export interface EquipableItem { type: "item"; attackModifier?: Range; @@ -211,10 +208,6 @@ export interface EquipableItem { modifyAttack?: (scene: FightScene) => void; } -function randomValue(range: Range) { - return Math.round(range.min + Math.random() * (range.max - range.min)); -} - function calcDamage(rawDamage: number, defence: number) { if (defence >= rawDamage) { return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; diff --git a/src/service/random.ts b/src/service/random.ts new file mode 100644 index 00000000..81e80ec5 --- /dev/null +++ b/src/service/random.ts @@ -0,0 +1,8 @@ +export interface Range { + min: number; + max: number; +} + +export function randomValue(range: Range): number { + return Math.round(range.min + Math.random() * (range.max - range.min)); +} From 6882cd7b97e9725de1f3c825e452c05b18fd77bf Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 14:58:23 +0100 Subject: [PATCH 21/26] Make random API more clear --- src/service/fightData.ts | 16 ++++++++-------- src/service/random.ts | 23 ++++++++++++++++++++--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 8a471e56..62614116 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -3,34 +3,34 @@ import { randomValue, type Range } from "@/service/random.js"; export const fightTemplates: { [name: string]: Equipable } = { ayran: { type: "item", - attackModifier: { min: 2, max: 3 }, + attackModifier: { min: 2, maxExclusive: 3 }, }, oettinger: { type: "item", - attackModifier: { min: 1, max: 5 }, - defenceModifier: { min: -3, max: 0 }, + attackModifier: { min: 1, maxExclusive: 5 }, + defenceModifier: { min: -3, maxExclusive: 0 }, }, thunfischshake: { type: "item", - attackModifier: { min: 3, max: 5 }, + attackModifier: { min: 3, maxExclusive: 5 }, }, nachthemd: { type: "armor", health: 50, - defence: { min: 2, max: 5 }, + defence: { min: 2, maxExclusive: 5 }, }, eierwaermer: { type: "armor", health: 30, - defence: { min: 3, max: 5 }, + defence: { min: 3, maxExclusive: 5 }, }, dildo: { type: "weapon", - attack: { min: 3, max: 9 }, + attack: { min: 3, maxExclusive: 9 }, }, messerblock: { type: "weapon", - attack: { min: 1, max: 9 }, + attack: { min: 1, maxExclusive: 9 }, }, }; export const bossMap: { [name: string]: Enemy } = { diff --git a/src/service/random.ts b/src/service/random.ts index 81e80ec5..1f46bccf 100644 --- a/src/service/random.ts +++ b/src/service/random.ts @@ -1,8 +1,25 @@ -export interface Range { +export type Range = ExclusiveRange | InclusiveRange; + +/** + * [min, ceil(maxExclusive)) + */ +export interface ExclusiveRange { + min: number; + maxExclusive: number; +} + +/** + * [min, ceil(maxInclusive)] + */ +export interface InclusiveRange { min: number; - max: number; + maxInclusive: number; } +/** + * Based on https://stackoverflow.com/a/24152886 + */ export function randomValue(range: Range): number { - return Math.round(range.min + Math.random() * (range.max - range.min)); + const upperLimit = "maxInclusive" in range ? range.maxInclusive + 1 : range.maxExclusive; + return Math.round(range.min + Math.random() * (upperLimit - range.min)); } From 853ce6a75687ff56ceb478156f95ad71978effba Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 15:07:32 +0100 Subject: [PATCH 22/26] Fix var casing --- src/storage/db/model.ts | 6 +++--- src/storage/fightHistory.ts | 8 ++++---- src/storage/fightInventory.ts | 8 ++++---- src/storage/migrations/11-fightsystem.ts | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index f3c21278..625811ed 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -292,15 +292,15 @@ export interface EmoteUseTable extends AuditedTable { interface FightHistoryTable extends AuditedTable { id: GeneratedAlways; - userid: Snowflake; + userId: Snowflake; result: boolean; bossName: string; - firsttime: boolean; + firstTime: boolean; } interface FightInventoryTable { id: GeneratedAlways; - userid: Snowflake; + userId: Snowflake; lootId: LootId; equippedSlot: string; } diff --git a/src/storage/fightHistory.ts b/src/storage/fightHistory.ts index 2cf33c64..92815d24 100644 --- a/src/storage/fightHistory.ts +++ b/src/storage/fightHistory.ts @@ -7,10 +7,10 @@ export async function insertResult(userId: User["id"], boss: string, win: boolea return await ctx .insertInto("fightHistory") .values({ - userid: userId, + userId: userId, result: win, bossName: boss, - firsttime: lastWins.length === 0 && win, + firstTime: lastWins.length === 0 && win, }) .execute(); } @@ -18,7 +18,7 @@ export async function insertResult(userId: User["id"], boss: string, win: boolea export async function getWinsForBoss(userId: User["id"], boss: string, ctx = db()) { return await ctx .selectFrom("fightHistory") - .where("userid", "=", userId) + .where("userId", "=", userId) .where("bossName", "=", boss) .where("result", "=", true) .selectAll() @@ -28,7 +28,7 @@ export async function getWinsForBoss(userId: User["id"], boss: string, ctx = db( export async function getLastFight(userId: User["id"], ctx = db()) { return await ctx .selectFrom("fightHistory") - .where("userid", "=", userId) + .where("userId", "=", userId) .orderBy("createdAt desc") .selectAll() .executeTakeFirst(); diff --git a/src/storage/fightInventory.ts b/src/storage/fightInventory.ts index 4a3d082f..5585a0e8 100644 --- a/src/storage/fightInventory.ts +++ b/src/storage/fightInventory.ts @@ -10,7 +10,7 @@ import { deleteLoot } from "@/storage/loot.js"; export async function getFightInventoryUnsorted(userId: User["id"], ctx = db()) { return await ctx .selectFrom("fightInventory") - .where("userid", "=", userId) + .where("userId", "=", userId) .selectAll() .execute(); } @@ -41,7 +41,7 @@ export async function getGameTemplate( export async function getItemsByType(userId: User["id"], fightItemType: string, ctx = db()) { return await ctx .selectFrom("fightInventory") - .where("userid", "=", userId) + .where("userId", "=", userId) .where("equippedSlot", "=", fightItemType) .selectAll() .execute(); @@ -55,7 +55,7 @@ export async function removeItemsAfterFight(userId: User["id"], ctx = db()) { } await ctx .deleteFrom("fightInventory") - .where("userid", "=", userId) + .where("userId", "=", userId) .where("equippedSlot", "=", "item") .execute(); }); @@ -89,7 +89,7 @@ export async function equipItembyLoot( await ctx .insertInto("fightInventory") .values({ - userid: userId, + userId: userId, lootId: loot.id, equippedSlot: itemType, }) diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts index 15c42761..d510fb5f 100644 --- a/src/storage/migrations/11-fightsystem.ts +++ b/src/storage/migrations/11-fightsystem.ts @@ -6,7 +6,7 @@ export async function up(db: Kysely) { .createTable("fightInventory") .ifNotExists() .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) - .addColumn("userid", "text") + .addColumn("userId", "text") .addColumn("lootId", "integer", c => c.references("loot.id")) .addColumn("equippedSlot", "text") .execute(); @@ -15,10 +15,10 @@ export async function up(db: Kysely) { .createTable("fightHistory") .ifNotExists() .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) - .addColumn("userid", "text", c => c.notNull()) + .addColumn("userId", "text", c => c.notNull()) .addColumn("bossName", "text", c => c.notNull()) .addColumn("result", "boolean", c => c.notNull()) - .addColumn("firsttime", "boolean", c => c.notNull()) + .addColumn("firstTime", "boolean", c => c.notNull()) .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .execute(); From d076d2ada407cb9df2186e8ac896fa85fece8492 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 15:10:07 +0100 Subject: [PATCH 23/26] Copy function --- src/storage/migrations/10-loot-attributes.ts | 2 +- src/storage/migrations/11-fightsystem.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/storage/migrations/10-loot-attributes.ts b/src/storage/migrations/10-loot-attributes.ts index 5cb0fb42..16cd9b6e 100644 --- a/src/storage/migrations/10-loot-attributes.ts +++ b/src/storage/migrations/10-loot-attributes.ts @@ -64,7 +64,7 @@ export async function up(db: Kysely) { } } -export function createUpdatedAtTrigger(db: Kysely, tableName: string) { +function createUpdatedAtTrigger(db: Kysely, tableName: string) { return sql .raw(` create trigger ${tableName}_updatedAt diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts index d510fb5f..2705e5ed 100644 --- a/src/storage/migrations/11-fightsystem.ts +++ b/src/storage/migrations/11-fightsystem.ts @@ -1,5 +1,4 @@ import { sql, type Kysely } from "kysely"; -import { createUpdatedAtTrigger } from "@/storage/migrations/10-loot-attributes.js"; export async function up(db: Kysely) { await db.schema @@ -26,6 +25,20 @@ export async function up(db: Kysely) { await createUpdatedAtTrigger(db, "fightHistory"); } +function createUpdatedAtTrigger(db: Kysely, tableName: string) { + return sql + .raw(` + create trigger ${tableName}_updatedAt + after update on ${tableName} for each row + begin + update ${tableName} + set updatedAt = current_timestamp + where id = old.id; + end; + `) + .execute(db); +} + export async function down(_db: Kysely) { throw new Error("Not supported lol"); } From 7178c3584d5cd76012ebca98a358f55e29d8114a Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 15:23:04 +0100 Subject: [PATCH 24/26] Fix var casing --- src/commands/fight.ts | 20 ++++++++++---------- src/service/fightData.ts | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/commands/fight.ts b/src/commands/fight.ts index 7a96d7c1..f495c41a 100644 --- a/src/commands/fight.ts +++ b/src/commands/fight.ts @@ -165,8 +165,8 @@ export async function fight( enemy: enemy, }; while (checkWin(scene) === undefined) { - player.itemtext = []; - enemy.itemtext = []; + player.itemText = []; + enemy.itemText = []; //playerhit first player.attack(enemy); // then enemny hit @@ -196,17 +196,17 @@ export async function fight( } function renderStats(player: Entity) { - while (player.itemtext.length < 5) { - player.itemtext.push("-"); + while (player.itemText.length < 5) { + player.itemText.push("-"); } return { name: player.stats.name, - value: `❤️HP${player.stats.health}/${player.maxhealth} - ❤️${"=".repeat(Math.max(0, (player.stats.health / player.maxhealth) * 10))} - ⚔️Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastattack} - 🛡️Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastdefence} - 📚Items: - ${player.itemtext.join("\n")} + value: `❤️ HP${player.stats.health}/${player.maxHealth} + ❤️ ${"=".repeat(Math.max(0, (player.stats.health / player.maxHealth) * 10))} + ⚔️ Waffe: ${player.stats.weapon?.name ?? "Schwengel"} ${player.lastAttack} + 🛡️ Rüstung: ${player.stats.armor?.name ?? "Nackt"} ${player.lastDefense} + 📚 Items: + ${player.itemText.join("\n")} `, inline: true, }; diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 62614116..1c07ce39 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -151,17 +151,17 @@ export interface Enemy extends BaseEntity { export class Entity { stats: BaseEntity; - maxhealth: number; - lastattack?: number; - lastdefence?: number; - itemtext: string[] = []; + maxHealth: number; + lastAttack?: number; + lastDefense?: number; + itemText: string[] = []; constructor(entity: BaseEntity) { this.stats = entity; if (this.stats.armor?.health) { this.stats.health += this.stats.armor?.health; } - this.maxhealth = this.stats.health; + this.maxHealth = this.stats.health; } attack(enemy: Entity) { @@ -181,7 +181,7 @@ export class Entity { `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, ); enemy.stats.health -= result.damage; - this.lastattack = result.rawDamage; + this.lastAttack = result.rawDamage; return result; } @@ -195,7 +195,7 @@ export class Entity { defence += randomValue(item.defenceModifier); } } - this.lastdefence = defence; + this.lastDefense = defence; return defence; } } From b9cc22d99fc8a4769901f351a2fdecbdd686e56f Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 23 Jan 2025 15:27:01 +0100 Subject: [PATCH 25/26] British english -> american english --- src/service/fightData.ts | 48 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/service/fightData.ts b/src/service/fightData.ts index 1c07ce39..67597177 100644 --- a/src/service/fightData.ts +++ b/src/service/fightData.ts @@ -8,7 +8,7 @@ export const fightTemplates: { [name: string]: Equipable } = { oettinger: { type: "item", attackModifier: { min: 1, maxExclusive: 5 }, - defenceModifier: { min: -3, maxExclusive: 0 }, + defenseModifier: { min: -3, maxExclusive: 0 }, }, thunfischshake: { type: "item", @@ -17,12 +17,12 @@ export const fightTemplates: { [name: string]: Equipable } = { nachthemd: { type: "armor", health: 50, - defence: { min: 2, maxExclusive: 5 }, + defense: { min: 2, maxExclusive: 5 }, }, eierwaermer: { type: "armor", health: 30, - defence: { min: 3, maxExclusive: 5 }, + defense: { min: 3, maxExclusive: 5 }, }, dildo: { type: "weapon", @@ -39,7 +39,7 @@ export const bossMap: { [name: string]: Enemy } = { description: "", health: 150, baseDamage: 2, - baseDefence: 0, + baseDefense: 0, enabled: true, armor: { name: "Nachthemd", @@ -59,7 +59,7 @@ export const bossMap: { [name: string]: Enemy } = { description: "", health: 120, baseDamage: 1, - baseDefence: 1, + baseDefense: 1, enabled: false, lossDescription: "", winDescription: "", @@ -71,7 +71,7 @@ export const bossMap: { [name: string]: Enemy } = { health: 120, enabled: false, baseDamage: 1, - baseDefence: 1, + baseDefense: 1, lossDescription: "", winDescription: "", items: [], @@ -83,7 +83,7 @@ export const bossMap: { [name: string]: Enemy } = { winDescription: "", health: 200, baseDamage: 3, - baseDefence: 5, + baseDefense: 5, enabled: false, items: [], }, @@ -97,7 +97,7 @@ export const bossMap: { [name: string]: Enemy } = { health: 350, enabled: false, baseDamage: 5, - baseDefence: 5, + baseDefense: 5, items: [], }, }; @@ -106,7 +106,7 @@ export const baseStats = { description: "", health: 80, baseDamage: 1, - baseDefence: 0, + baseDefense: 0, }; export type FightItemType = "weapon" | "armor" | "item"; @@ -120,7 +120,7 @@ export interface EquipableWeapon { export interface EquipableArmor { type: "armor"; - defence: Range; + defense: Range; health: number; } @@ -134,7 +134,7 @@ export interface BaseEntity { name: string; description: string; baseDamage: number; - baseDefence: number; + baseDefense: number; items: (EquipableItem & { name: string })[]; weapon?: EquipableWeapon & { name: string }; @@ -175,8 +175,8 @@ export class Entity { rawDamage += randomValue(value1.attackModifier); } } - const defence = enemy.defend(); - const result = calcDamage(rawDamage, defence); + const defense = enemy.defend(); + const result = calcDamage(rawDamage, defense); console.log( `${this.stats.name} (${this.stats.health}) hits ${enemy.stats.name} (${enemy.stats.health}) for ${result.damage} mitigated ${result.mitigated}`, ); @@ -186,31 +186,31 @@ export class Entity { } defend() { - let defence = this.stats.baseDefence; - if (this.stats.armor?.defence) { - defence += randomValue(this.stats.armor.defence); + let defense = this.stats.baseDefense; + if (this.stats.armor?.defense) { + defense += randomValue(this.stats.armor.defense); } for (const item of this.stats.items) { - if (item.defenceModifier) { - defence += randomValue(item.defenceModifier); + if (item.defenseModifier) { + defense += randomValue(item.defenseModifier); } } - this.lastDefense = defence; - return defence; + this.lastDefense = defense; + return defense; } } export interface EquipableItem { type: "item"; attackModifier?: Range; - defenceModifier?: Range; + defenseModifier?: Range; afterFight?: (scene: FightScene) => void; modifyAttack?: (scene: FightScene) => void; } -function calcDamage(rawDamage: number, defence: number) { - if (defence >= rawDamage) { +function calcDamage(rawDamage: number, defense: number) { + if (defense >= rawDamage) { return { rawDamage: rawDamage, damage: 0, mitigated: rawDamage }; } - return { rawDamage: rawDamage, damage: rawDamage - defence, mitigated: defence }; + return { rawDamage: rawDamage, damage: rawDamage - defense, mitigated: defense }; } From 4b8f184638cef12e71488764f7e75946cff25958 Mon Sep 17 00:00:00 2001 From: Feluin Date: Fri, 20 Jun 2025 19:38:39 +0200 Subject: [PATCH 26/26] added karte --- assets/maps/CSZ Karte V1.png | Bin 0 -> 38008 bytes src/commands/karte.ts | 155 +++++++++++++++++++++++ src/storage/db/model.ts | 15 ++- src/storage/mapPosition.ts | 91 +++++++++++++ src/storage/migrations/11-fightsystem.ts | 12 +- 5 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 assets/maps/CSZ Karte V1.png create mode 100644 src/commands/karte.ts create mode 100644 src/storage/mapPosition.ts diff --git a/assets/maps/CSZ Karte V1.png b/assets/maps/CSZ Karte V1.png new file mode 100644 index 0000000000000000000000000000000000000000..bb5945ec6dac7d477c1a4ea8d799647d10a4ba94 GIT binary patch literal 38008 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, context: BotContext) { + if (!command.isChatInputCommand()) { + // TODO: Solve this on a type level + return; + } + + const author = command.guild?.members.resolve(command.user); + + if (!author) { + throw new Error("Couldn't resolve guild member"); + } + + const directions = [["NW", "N", "NE"], ["W", "X", "E"], ["SW", "S", "SE"]]; + + const rows = []; + for (const directionrow of directions) { + const row = new ActionRowBuilder(); + for (const direction of directionrow) { + const button = new ButtonBuilder() + .setCustomId("map_" + direction) + .setLabel(direction); + if (direction === "X") { + button.setStyle(ButtonStyle.Danger); + } + else { + button.setStyle(ButtonStyle.Secondary); + } + row.addComponents(button); + } + rows.push(row); + } + const map = await this.buildMap(await getPositionForUser(author.id), command.user, context); + + + const sentReply = await command.reply({ + fetchReply: true, + embeds: [ + { + title: `Karte des heiligen CSZ Landes`, + description: ``, + color: 0x00ff00, + + image: { + url: `attachment://Karte.png` + } + } + ], + files: [ + { + name: "Karte.png", + attachment: map + } + ], + components: rows + }); + const collector = sentReply.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 45_000 + }); + collector.on("collect", async i => { + const playerpos = await move(i.user.id, i.customId.valueOf().replace("map_", "") as Direction); + await i.message.edit({ + files: [ + { + name: "Karte.png", + attachment: await this.buildMap(playerpos, i.user, context) + } + ] + }); + await i.deferUpdate(); + }); + collector.on("dispose", async i => { + await i.deleteReply("@original"); + }); + + } + + private async buildMap(position: MapPosition, user: User, context: BotContext): Promise { + + const background = await fs.readFile("assets/maps/CSZ Karte V1.png"); + const backgroundImage = await loadImage(background); + const canvas = createCanvas(backgroundImage.width, backgroundImage.height); + const ctx = canvas.getContext("2d"); + ctx.drawImage(backgroundImage, 0, 0); + + let allPostions = await getAllPostions(); + for (const pos of allPostions) { + let member = context.guild.members.cache.find(m => m.id === pos.userid); + if (member && pos.userid != user.id) { + await this.drawPlayer(ctx, pos, member.user, "small"); + } + } + await this.drawPlayer(ctx, position, user, "large"); + return canvas.toBuffer("image/png"); + } + + private async drawPlayer(ctx: SKRSContext2D, position: MapPosition, user: User, size: "small" | "large") { + + + ctx.beginPath(); + ctx.strokeStyle = size == "large" ? "blue" : "grey"; + ctx.lineWidth = size == "large" ? 3 : 1; + const radius = size == "large" ? 32 : 16; + ctx.arc(position.x * stepfactor + radius, position.y * stepfactor + radius, radius, 0, 2 * Math.PI); + ctx.stroke(); + let textMetrics = ctx.measureText(position.userid); + //Todo here funny pixelcounting to center the text + ctx.strokeStyle = "blue"; + ctx.lineWidth = 1; + + ctx.strokeText(user.displayName, position.x * stepfactor, position.y * stepfactor +( size == "large" ? 75 : 40)); + const avatarURL = user.avatarURL({ + size: size == "large" ? 64 : 32 + + }); + let avatar = await loadImage(avatarURL!); + ctx.save(); + ctx.beginPath(); + ctx.arc(position.x * stepfactor + radius, position.y * stepfactor + radius, radius, 0, 2 * Math.PI); + ctx.closePath(); + ctx.clip(); + + ctx.drawImage(avatar, position.x * stepfactor, position.y * stepfactor); + ctx.restore(); + } +} + +export type Direction = "NW" | "N" | "NE" | "W" | "X" | "E" | "SW" | "S" | "SE"; + +const stepfactor = 32; + diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index 51319959..4c29ba86 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -1,7 +1,8 @@ -import type { Snowflake } from "discord.js"; -import type { ColumnType, Generated, GeneratedAlways, Insertable, Selectable } from "kysely"; +import type {Snowflake} from "discord.js"; +import type {ColumnType, Generated, GeneratedAlways, Insertable, Selectable} from "kysely"; + +import type {Radius} from "@/commands/penis.js"; -import type { Radius } from "@/commands/penis.js"; export interface Database { birthdays: BirthdayTable; @@ -26,6 +27,7 @@ export interface Database { emoteUse: EmoteUseTable; fighthistory: FightHistoryTable; fightinventory: FightInventoryTable; + position: MapPositonTable; } export type OneBasedMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; @@ -304,3 +306,10 @@ interface FightInventoryTable { lootId: LootId; equippedSlot: string; } + +export interface MapPositonTable { + id: GeneratedAlways; + userid: Snowflake; + x: number, + y: number, +} diff --git a/src/storage/mapPosition.ts b/src/storage/mapPosition.ts new file mode 100644 index 00000000..40272f5a --- /dev/null +++ b/src/storage/mapPosition.ts @@ -0,0 +1,91 @@ +import type {User} from "discord.js"; +import db from "@db"; +import {Direction} from "@/commands/karte.js"; + +export async function getPositionForUser(userId: User["id"], ctx = db()): Promise { + const pos = await ctx + .selectFrom("position") + .where("userid", "=", userId) + .selectAll() + .executeTakeFirst(); + if (pos != null) { + return pos; + } + await ctx + .insertInto("position") + .values({ + userid: userId, + x: startx, + y: starty + }) + .execute(); + return await ctx + .selectFrom("position") + .where("userid", "=", userId) + .selectAll() + .executeTakeFirstOrThrow(); +} + +const startx = 0, starty = 0; + +export interface MapPosition { + id: number; + userid: User["id"]; + x: number; + y: number; +} + +export async function getAllPostions(ctx = db()): Promise { + return await ctx + .selectFrom("position") + .selectAll() + .execute(); +} + +async function savePos(pos: MapPosition, ctx = db()) { + await ctx.updateTable("position").where("userid", "=", pos.userid) + .set({ + x: pos.x, + y: pos.y + }) + .execute(); +} + +export async function move(userId: User["id"], direction: Direction) { + let pos = await getPositionForUser(userId); + switch (direction) { + case "NW": + pos.x = pos.x - 1; + pos.y = pos.y - 1; + break; + case "N": + pos.y = pos.y - 1; + break; + case "NE": + pos.y = pos.y - 1; + pos.x = pos.x + 1; + break; + case "W": + pos.x = pos.x - 1; + break; + case "X": + break; + case "E": + pos.x = pos.x + 1; + + break; + case "SW": + pos.x = pos.x - 1; + pos.y = pos.y + 1; + break; + case "S": + pos.y = pos.y + 1; + break; + case "SE": + pos.y = pos.y + 1; + pos.x = pos.x + 1; + break; + } + await savePos(pos); + return pos; +} diff --git a/src/storage/migrations/11-fightsystem.ts b/src/storage/migrations/11-fightsystem.ts index 2a8d0d04..5eeae3f1 100644 --- a/src/storage/migrations/11-fightsystem.ts +++ b/src/storage/migrations/11-fightsystem.ts @@ -1,5 +1,5 @@ -import { sql, type Kysely } from "kysely"; -import { createUpdatedAtTrigger } from "@/storage/migrations/10-loot-attributes.js"; +import {sql, type Kysely} from "kysely"; +import {createUpdatedAtTrigger} from "@/storage/migrations/10-loot-attributes.js"; export async function up(db: Kysely) { await db.schema @@ -22,6 +22,14 @@ export async function up(db: Kysely) { .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) .execute(); await createUpdatedAtTrigger(db, "fighthistory"); + await db.schema + .createTable("position") + .ifNotExists() + .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) + .addColumn("userid", "text", c => c.notNull()) + .addColumn("x", "integer", c => c.notNull()) + .addColumn("y", "integer", c => c.notNull()) + .execute(); } export async function down(_db: Kysely) {