diff --git a/src/MooLite/core/MooLite.ts b/src/MooLite/core/MooLite.ts index cb2bf56..e02a9ec 100644 --- a/src/MooLite/core/MooLite.ts +++ b/src/MooLite/core/MooLite.ts @@ -22,6 +22,7 @@ import { EquipmentBuffsUpdatedParser } from "src/MooLite/core/server/messages/Eq import { ItemsUpdatedParser } from "src/MooLite/core/server/messages/ItemsUpdated"; import { LootOpenedParser } from "src/MooLite/core/server/messages/LootOpened"; import { AbilitiesUpdatedParser } from "src/MooLite/core/server/messages/AbilitiesUpdated"; +import { InitClientInfoParser } from "./server/messages/InitClientInfo"; import { unsafeWindow } from "$"; export class MooLite { @@ -33,6 +34,7 @@ export class MooLite { messageParsers: MessageParser[] = [ // Server messages new InitCharacterInfoParser(), + new InitClientInfoParser(), new PongParser(), new ActionCompletedParser(), @@ -171,8 +173,7 @@ export class MooLite { }); if (!parser) { if (!isClientMessage) { - console.warn(`Unhandled message type ${message.type}`); - console.log(message); + console.warn(`Unhandled message type ${message.type}`, message); } return; } diff --git a/src/MooLite/core/inventory/items/decodeItemHash.ts b/src/MooLite/core/inventory/items/decodeItemHash.ts new file mode 100644 index 0000000..b08ad52 --- /dev/null +++ b/src/MooLite/core/inventory/items/decodeItemHash.ts @@ -0,0 +1,29 @@ +import { ItemHrid } from "../ItemHrid"; +import { ItemLocationHrid } from "../ItemLocationHrid"; + +export interface ItemHashParts { + characterId: number; + itemLocationHrid: ItemLocationHrid; + itemHrid: ItemHrid; + enhancementLevel: number; +} + +/** + * An item hash looks like + * `123456::/item_locations/inventory::/items/cheese_enhancer::0` + * This function will break it down into its component parts so they can be + * used elsewhere, like when tracking what items are being used for enhancing. + * + * @param itemHash - the hash to deconstruct, should be in the above format + * @returns a deconstructed item hash + */ +export function decodeItemHash(itemHash: string): ItemHashParts { + const parts = itemHash.split("::"); + + return { + characterId: parseInt(parts[0]), + itemLocationHrid: parts[1] as any as ItemLocationHrid, + itemHrid: parts[2] as any as ItemHrid, + enhancementLevel: parseInt(parts[3]), + }; +} diff --git a/src/MooLite/core/server/clientmessages/ClientMessageType.ts b/src/MooLite/core/server/clientmessages/ClientMessageType.ts index 0081d5f..d09e4a0 100644 --- a/src/MooLite/core/server/clientmessages/ClientMessageType.ts +++ b/src/MooLite/core/server/clientmessages/ClientMessageType.ts @@ -1,3 +1,3 @@ export enum ClientMessageType { - Ping = "/character_tasks/ping", + Ping = "ping", } diff --git a/src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPlugin.ts b/src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPlugin.ts new file mode 100644 index 0000000..8a334b7 --- /dev/null +++ b/src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPlugin.ts @@ -0,0 +1,130 @@ +import { MooLitePlugin } from "src/MooLite/core/plugins/MooLitePlugin"; +import { MooLiteTab } from "src/MooLite/core/plugins/MooLiteTab"; +import EnhancingTrackerPluginDisplay from "src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPluginDisplay.vue"; +import { markRaw } from "vue"; +import { PluginAuthorCredits } from "src/MooLite/core/plugins/PluginAuthorCredits"; +import { ItemDetail } from "src/MooLite/core/inventory/items/ItemDetail"; +import { ItemHrid } from "src/MooLite/core/inventory/ItemHrid"; +import { CharacterAction } from "src/MooLite/core/actions/CharacterAction"; +import { ItemHashParts, decodeItemHash } from "src/MooLite/core/inventory/items/decodeItemHash"; +import { ItemAmount } from "src/MooLite/core/inventory/items/ItemAmount"; +import { ActionHrid } from "src/MooLite/core/actions/ActionHrid"; + +export class EnhancingTrackerPlugin extends MooLitePlugin { + name: string = "Enhancing Tracker"; + key = "enhancing-tracker"; + description: string = "Tracks materials consumed during enhancing."; + + credits: PluginAuthorCredits = { + author: "seashairo", + maintainer: "seashairo", + }; + + tab: MooLiteTab = { + icon: "⭐", + pluginName: this.name, + componentName: "EnhancingTrackerPluginDisplay", + component: markRaw(EnhancingTrackerPluginDisplay), + }; + + public get itemDetailMap(): Record { + return this._game.inventory.itemDetailMap; + } + + itemBeingEnhancedHrid: ItemHrid | null = null; + itemsConsumed: Record = {}; + bestEnhancementLevel: number = 0; + successes: number = 0; + failures: number = 0; + blessedTeaProcs: number = 0; + itemState: ItemHashParts | null = null; + previousAction: CharacterAction | null = null; + + public get successRate(): number { + const attempts = this.successes + this.failures; + return attempts === 0 ? 0 : (this.successes / attempts) * 100; + } + + onActionQueueUpdated(queue: CharacterAction[]): void { + const currentAction = queue.length ? queue[0] : null; + + const enhancing = "/actions/enhancing/enhance" as unknown as ActionHrid; + const currentActionIsEnhancing = currentAction?.actionHrid === enhancing; + const previousActionIsEnhancing = this.previousAction?.actionHrid === enhancing; + + if (currentActionIsEnhancing) { + previousActionIsEnhancing + ? this.handleEnhancing(currentAction) + : this.handleStartedEnhancing(currentAction); + } + + this.previousAction = currentAction; + } + + handleStartedEnhancing(currentAction: CharacterAction): void { + const itemHashParts = decodeItemHash(currentAction.upgradeItemHash); + this.itemState = itemHashParts; + this.itemBeingEnhancedHrid = itemHashParts.itemHrid; + this.bestEnhancementLevel = itemHashParts.enhancementLevel; + this.itemsConsumed = {}; + } + + addConsumedItem(itemAmount: ItemAmount): void { + itemAmount.itemHrid in this.itemsConsumed + ? (this.itemsConsumed[itemAmount.itemHrid] += itemAmount.count) + : (this.itemsConsumed[itemAmount.itemHrid] = itemAmount.count); + } + + handleEnhancing(currentAction: CharacterAction): void { + const currentItemState = decodeItemHash(currentAction.upgradeItemHash); + const itemBeingEnhancedHrid = currentItemState.itemHrid; + + // If the item being enhanced has changed hrid, then we're enhancing + // something new. + // + // TODO: Handle this more gracefully. Maybe store a dict of enhancing + // data against hrid to display a table? + if (this.itemBeingEnhancedHrid !== itemBeingEnhancedHrid) { + return this.handleStartedEnhancing(currentAction); + } + + // Instead of watching the items gained and lost, we want to use the + // item's enhancement cost. This is because players using the market + // will hit the onItemGained handler and leave the count inaccurate. + // + // TODO: When Blessed Tea procs, do you use 1 or 2 sets of consumables? + // If it's 1 then this is accurate, but if it's 2 then this logic would + // need to be part of the level check below + const enhancingCosts = this._game.inventory.itemDetailMap[itemBeingEnhancedHrid].enhancementCosts; + enhancingCosts!.forEach((itemAmount) => this.addConsumedItem(itemAmount)); + + const itemState = this.itemState!; + if (currentItemState.enhancementLevel > itemState.enhancementLevel) { + const diff = currentItemState.enhancementLevel - itemState.enhancementLevel; + this.successes += 1; + // I'm pretty sure the level diff will only ever be 1 or 2, but just + // in case blessed tea can multi-proc we'll check for anything + // greater than 1. I'm also assuming (but haven't confirmed) that a + // blessed tea proc will still be in a single action update and not + // two separate ones. + if (diff > 1) { + this.blessedTeaProcs += 1; + } + } else { + this.failures += 1; + + // Protections don't get counted in the regular consumed items + // report, so if we drop an enhancement level then we need to + // manually count a lost protection. + const protectionHash = currentAction.enhancingProtectionItemHash; + if (protectionHash !== "" && itemState.enhancementLevel >= currentAction.enhancingProtectionItemMinLevel) { + const itemHrid = decodeItemHash(protectionHash).itemHrid; + this.addConsumedItem({ itemHrid, count: 1 }); + } + } + + this.bestEnhancementLevel = Math.max(currentItemState.enhancementLevel, this.bestEnhancementLevel); + + this.itemState = currentItemState; + } +} diff --git a/src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPluginDisplay.vue b/src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPluginDisplay.vue new file mode 100644 index 0000000..f4b5095 --- /dev/null +++ b/src/MooLite/plugins/EnhancingTracker/EnhancingTrackerPluginDisplay.vue @@ -0,0 +1,52 @@ + + + diff --git a/src/components/atoms/ItemIcon.vue b/src/components/atoms/ItemIcon.vue index e8c941b..dd5ff69 100644 --- a/src/components/atoms/ItemIcon.vue +++ b/src/components/atoms/ItemIcon.vue @@ -15,7 +15,7 @@ const imgUrl = computed(() => { const split = (props.item as unknown as string).split("/"); const itemPostfix = split[split.length - 1]; // TODO(@Isha): Research how these CDN resource urls are generated - return `/static/media/items_sprite.951ef1ec.svg#${itemPostfix}`; + return `/static/media/items_sprite.018a3c6e.svg#${itemPostfix}`; }); diff --git a/src/main.ts b/src/main.ts index e9d4dd8..67f44b9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,7 @@ import { ConsumableNotifierPlugin } from "./MooLite/plugins/ConsumableNotifier/C import { EquipmentExporterPlugin } from "src/MooLite/plugins/EquipmentExporter/EquipmentExporterPlugin"; import { WhisperManagerPlugin } from "./MooLite/plugins/WhisperManager/WhisperManagerPlugin"; import { ThemesPlugin } from "src/MooLite/plugins/Themes/ThemesPlugin"; +import { EnhancingTrackerPlugin } from "./MooLite/plugins/EnhancingTracker/EnhancingTrackerPlugin"; declare global { interface Window { @@ -61,6 +62,7 @@ const launchMooLite = () => { new EquipmentExporterPlugin(), new WhisperManagerPlugin(), new ThemesPlugin(), + new EnhancingTrackerPlugin(), ]) as unknown as MooLitePlugin[]; const pluginManager = reactive(new PluginManager(game, plugins)) as PluginManager;