diff --git a/src/MooLite/core/Game.ts b/src/MooLite/core/Game.ts index aee20d7..d4e8c01 100644 --- a/src/MooLite/core/Game.ts +++ b/src/MooLite/core/Game.ts @@ -9,6 +9,7 @@ import { Combat } from "src/MooLite/core/combat/Combat"; import { Leaderboard } from "src/MooLite/core/leaderboard/Leaderboard"; import { Equipment } from "src/MooLite/core/equipment/Equipment"; import { LootBoxes } from "src/MooLite/core/lootboxes/LootBoxes"; +import { Enhancing } from "src/MooLite/core/enhancing/Enhancing"; export class Game { gameVersion: string; @@ -23,6 +24,7 @@ export class Game { chat: Chat; actionQueue: ActionQueue; inventory: Inventory; + enhancing: Enhancing; notifier: Notifier; @@ -58,6 +60,10 @@ export class Game { clientInfo.itemCategoryDetailMap, clientInfo.itemLocationDetailMap ); + this.enhancing = new Enhancing( + clientInfo.enhancementLevelSuccessRateTable, + clientInfo.enhancementLevelTotalBonusMultiplierTable + ); this.notifier = new Notifier(); } diff --git a/src/MooLite/core/enhancing/Enhancing.ts b/src/MooLite/core/enhancing/Enhancing.ts new file mode 100644 index 0000000..ad4a227 --- /dev/null +++ b/src/MooLite/core/enhancing/Enhancing.ts @@ -0,0 +1,145 @@ +import { ItemDetail } from "src/MooLite/core/inventory/items/ItemDetail"; +import { Math as MathUtil } from "src/MooLite/util/Math"; +import { EnhancingConstants } from "src/MooLite/core/enhancing/EnhancingConstants"; + +export class Enhancing { + public readonly enhancementLevelSuccessRateTable: number[]; + public readonly enhancementLevelTotalBonusMultiplierTable: number[]; + + constructor(enhancementLevelSuccessRateTable: number[], enhancementLevelTotalBonusMultiplierTable: number[]) { + this.enhancementLevelSuccessRateTable = enhancementLevelSuccessRateTable; + this.enhancementLevelTotalBonusMultiplierTable = enhancementLevelTotalBonusMultiplierTable; + } + + getSuccessChanceTable(currentEnhancingLevel: number, currentItemLevel: number, toolBonus: number): number[] { + let successChanceTable: number[] = [0]; + for (let i = 0; i < this.enhancementLevelSuccessRateTable.length; i++) { + let baseChance = this.enhancementLevelSuccessRateTable.at(i); + const levelBuff = + EnhancingConstants.levelBuffMultiplier * Math.max(currentEnhancingLevel - currentItemLevel, 0); + const levelDebuff = Math.min((currentEnhancingLevel / currentItemLevel + 1) / 2, 1); + if (baseChance != undefined) { + const actualChance = baseChance * (toolBonus + levelBuff + levelDebuff); + successChanceTable.push(actualChance); + } + } + successChanceTable.push(successChanceTable[successChanceTable.length - 1]); + return successChanceTable; + } + + getZScoreTable(successChanceTable: number[]): number[] { + let zScoreTable = []; + for (let i = 0; i < successChanceTable.length - 1; i++) { + const zScore = (1 - successChanceTable[i + 1]) * MathUtil.multiplyArray(successChanceTable.slice(1, i + 1)); + zScoreTable.push(zScore); + } + return zScoreTable; + } + + getSTable(successChanceTable: number[]): number[] { + let sTable = [1 - successChanceTable[1]]; + for (let i = 1; i < successChanceTable.length - 1; i++) { + const sValue = MathUtil.multiplyArray(successChanceTable.slice(1, i + 1)); + sTable.push(sValue); + } + return sTable; + } + + getCostTable(zScoreTable: number[], sTable: number[], materialCost: number): number[] { + let costs = [0]; + let multArray = []; + for (let i = 0; i < zScoreTable.length; i++) { + multArray[i] = zScoreTable[i] * (i + 1); + } + let sumTable = MathUtil.sumArray(multArray); + for (let i = 1; i < sTable.length; i++) { + const cost = ((sumTable[i - 1] + sTable[i] * i) / sTable[i]) * materialCost; + costs.push(cost); + } + return costs; + } + + getActionsRequiredTable( + costTable: number[], + successChanceTable: number[], + protectLevel: number, + enhancementCost: number + ): number[] { + let actions = [0]; + for (let i = 1; i < successChanceTable.length; i++) { + let currentActionAmount = 0; + if (protectLevel >= i) { + currentActionAmount = costTable[i] / enhancementCost; + } else { + currentActionAmount = + (actions[i - 1] + 1 - (1 - successChanceTable[i]) * actions[i - 2]) / successChanceTable[i]; + } + actions.push(currentActionAmount); + } + return actions; + } + + getProtectsRequiredTable( + costTable: number[], + actionAmountTable: number[], + protectCost: number, + enhanceCost: number + ): number[] { + let protects = []; + for (let i = 0; i < costTable.length; i++) { + protects.push((costTable[i] - actionAmountTable[i] * enhanceCost) / protectCost); + } + return protects; + } + + getBlessedTeaTable(successTable: number[], blessedTeaBoost: number): number[] { + let blessedTeaTable = [1, 1]; + for (let i = 2; i < successTable.length; i++) { + blessedTeaTable.push( + ((1 - blessedTeaBoost) ** 2 + + blessedTeaBoost * (1 - blessedTeaBoost) * (1 - successTable[i]) + + blessedTeaBoost / successTable[i - 1]) * + blessedTeaTable[i - 1] + ); + } + return blessedTeaTable; + } + + getCostWithProtects( + costTable: number[], + successChanceTable: number[], + protectCost: number, + materialCost: number + ): number[] { + let costsWithProtects = [costTable[0], costTable[1]]; + for (let i = 2; i < successChanceTable.length; i++) { + const cost = Math.min( + costTable[i], + (costsWithProtects[i - 1] + + (protectCost * (1 - successChanceTable[i]) + materialCost) - + (1 - successChanceTable[i]) * costsWithProtects[i - 2]) / + successChanceTable[i] + ); + costsWithProtects.push(cost); + } + return costsWithProtects; + } + + recommendedProtectLevel(costTable: number[], costWithProtectTable: number[]): number { + for (let i = 2; i < costTable.length; i++) { + if (costTable[i] > costWithProtectTable[i]) { + return i - 1; + } + } + return 20; + } + + getEnhancingToolBonus(enhancementLevel: number, itemDetails: ItemDetail): number { + const enhancementLevelMulti = this.enhancementLevelTotalBonusMultiplierTable; + const baseBonus = itemDetails.equipmentDetail.noncombatStats.enhancingSuccess; + const enhancementBonus = + itemDetails.equipmentDetail.noncombatEnhancementBonuses.enhancingSuccess * + enhancementLevelMulti[enhancementLevel]; + return baseBonus + enhancementBonus; + } +} diff --git a/src/MooLite/core/enhancing/EnhancingConstants.ts b/src/MooLite/core/enhancing/EnhancingConstants.ts new file mode 100644 index 0000000..97f0262 --- /dev/null +++ b/src/MooLite/core/enhancing/EnhancingConstants.ts @@ -0,0 +1,7 @@ +export class EnhancingConstants { + static readonly actionSpeed = 12; + static readonly experienceBonus = 14; + static readonly experiencePerLevel = 1.4; + static readonly failureExperienceRate = 0.1; + static readonly levelBuffMultiplier = 0.0005; +} diff --git a/src/MooLite/core/equipment/EquipmentDetail.ts b/src/MooLite/core/equipment/EquipmentDetail.ts index d3de2ca..df38c69 100644 --- a/src/MooLite/core/equipment/EquipmentDetail.ts +++ b/src/MooLite/core/equipment/EquipmentDetail.ts @@ -7,7 +7,7 @@ export interface EquipmentDetail { combatEnhancementBonuses: CombatStats; combatStats: CombatStats; levelRequirements: LevelRequirement[] | null; - nonCombatEnhancementBonuses: NonCombatStats; - nonCombatStats: NonCombatStats; + noncombatEnhancementBonuses: NonCombatStats; + noncombatStats: NonCombatStats; type: EquipmentTypeHrid; } diff --git a/src/MooLite/core/inventory/Inventory.ts b/src/MooLite/core/inventory/Inventory.ts index 4488d3c..d1521dd 100644 --- a/src/MooLite/core/inventory/Inventory.ts +++ b/src/MooLite/core/inventory/Inventory.ts @@ -160,4 +160,15 @@ export class Inventory { public getEquippedDrinks(): Record { return this._characterDrinks; } + + public getEnhancingTool(): CharacterItem | null { + const allItems = this._characterItems; + let index = allItems.findIndex( + (item: CharacterItem) => item.itemLocationHrid.toString() === "/item_locations/enhancing_tool" + ); + if (index != -1) { + return allItems[index]; + } + return null; + } } diff --git a/src/MooLite/core/inventory/items/Buffs.ts b/src/MooLite/core/inventory/items/Buffs.ts new file mode 100644 index 0000000..789e83f --- /dev/null +++ b/src/MooLite/core/inventory/items/Buffs.ts @@ -0,0 +1,13 @@ +import { BuffTypeHrid } from "src/MooLite/core/combat/buffs/BuffTypeHrid"; +import { BuffUniqueHrid } from "src/MooLite/core/combat/buffs/BuffUniqueHrid"; + +export interface Buffs { + duration: number; + flatBoost: number; + flatBoostLevelBonus: number; + ratioBoost: number; + ratioBoostLevelBonus: number; + startTime: Date; + typeHrid: BuffTypeHrid; + uniqueHrid: BuffUniqueHrid; +} diff --git a/src/MooLite/core/inventory/items/ConsumableDetail.ts b/src/MooLite/core/inventory/items/ConsumableDetail.ts index f8991a6..a4c661a 100644 --- a/src/MooLite/core/inventory/items/ConsumableDetail.ts +++ b/src/MooLite/core/inventory/items/ConsumableDetail.ts @@ -1,8 +1,8 @@ import { CombatTrigger } from "src/MooLite/core/combat/triggers/CombatTrigger"; +import { Buffs } from "src/MooLite/core/inventory/items/Buffs"; export interface ConsumableDetail { - // TODO(@Isha): Or what? - buffs: null; + buffs: Buffs[]; cooldownDuration: number; defaultCombatTriggers: CombatTrigger[] | null; hitpointsRestore: number; diff --git a/src/MooLite/core/server/messages/InitClientInfo.ts b/src/MooLite/core/server/messages/InitClientInfo.ts index 9f08218..2ff957d 100644 --- a/src/MooLite/core/server/messages/InitClientInfo.ts +++ b/src/MooLite/core/server/messages/InitClientInfo.ts @@ -51,8 +51,8 @@ export interface InitClientInfoMessage extends ServerMessage { // cowbellBundleDetailMap: Record; // currentTimestamp: string; // damageTypeDetailMap: Record; - // enhancementLevelSuccessRateTable: number; - // enhancementLevelTotalBonusMultiplierTable: number; + enhancementLevelSuccessRateTable: number[]; + enhancementLevelTotalBonusMultiplierTable: number[]; equipmentTypeDetailMap: Record; gameVersion: string; itemCategoryDetailMap: Record; diff --git a/src/MooLite/core/skills/Skills.ts b/src/MooLite/core/skills/Skills.ts index 07407e6..f660e8b 100644 --- a/src/MooLite/core/skills/Skills.ts +++ b/src/MooLite/core/skills/Skills.ts @@ -89,4 +89,15 @@ export class Skills { public getLevel(skillHrid: SkillHrid) { return this._characterSkills[skillHrid].level; } + + public getEnhancingLevel() { + return this._characterSkills["/skills/enhancing"].level; + } + + public getSkillEfficiencyBonusRatio(requiredLevel: number, currentLevel: number): number { + if (requiredLevel >= currentLevel) { + return 0; + } + return (currentLevel - requiredLevel) / 100; + } } diff --git a/src/MooLite/plugins/EnhancerHelper/EnhancerHelperPlugin.ts b/src/MooLite/plugins/EnhancerHelper/EnhancerHelperPlugin.ts new file mode 100644 index 0000000..233f34d --- /dev/null +++ b/src/MooLite/plugins/EnhancerHelper/EnhancerHelperPlugin.ts @@ -0,0 +1,221 @@ +import { MooLitePlugin } from "src/MooLite/core/plugins/MooLitePlugin"; +import { MooLiteTab } from "src/MooLite/core/plugins/MooLiteTab"; +import { markRaw } from "vue"; +import EnhancerHelperPluginDisplay from "src/MooLite/plugins/EnhancerHelper/EnhancerHelperPluginDisplay.vue"; +import { ItemDetail } from "src/MooLite/core/inventory/items/ItemDetail"; +import { ItemAmount } from "src/MooLite/core/inventory/items/ItemAmount"; +import { ItemHrid } from "src/MooLite/core/inventory/ItemHrid"; +import { CharacterItem } from "src/MooLite/core/inventory/CharacterItem"; +import { DateFormatter } from "src/MooLite/util/DateFormatter"; +import { EnhancingConstants } from "src/MooLite/core/enhancing/EnhancingConstants"; + +export class EnhancerHelperPlugin extends MooLitePlugin { + name: string = "Enhancer Helper"; + key = "enhancer-helper"; + description: string = "Various enhancing tools - By Void"; + + tab: MooLiteTab = { + icon: "📡", + pluginName: this.name, + componentName: "EnhancerHelperPluginDisplay", + component: markRaw(EnhancerHelperPluginDisplay), + }; + + getEnhanceableItems(): ItemDetail[] { + return this._game.inventory.sortedAlphabeticalItems.filter((item) => item.enhancementCosts != null); + } + + getEnhancementMaterials(itemHrid: ItemHrid): ItemAmount[] | undefined | null { + return this._game.inventory.sortedAlphabeticalItems.find((item) => item.hrid === itemHrid)?.enhancementCosts; + } + + getItemName(itemHrid: ItemHrid): String | null { + return this._game.inventory.itemDetailMap[itemHrid].name; + } + + getEnhancingLevel(): number { + return this._game.skills.getEnhancingLevel(); + } + + getEnhancementSuccessTable( + enhancingLevel: number, + itemLevel: number, + toolBonus: number, + useEnhancingTea: boolean, + useSuperEnhancingTea: boolean + ): number[] { + let enhancementLevelBonus = 0; + const enhancingTeaBuff = + this._game.inventory.itemDetailMap[("/items/enhancing_tea")].consumableDetail.buffs[0] + .flatBoost; + const superEnhancingTeaBuff = + this._game.inventory.itemDetailMap[("/items/super_enhancing_tea")].consumableDetail + .buffs[0].flatBoost; + if (useSuperEnhancingTea) { + enhancementLevelBonus = superEnhancingTeaBuff; + } else if (useEnhancingTea) { + enhancementLevelBonus = enhancingTeaBuff; + } + return this._game.enhancing.getSuccessChanceTable(enhancingLevel + enhancementLevelBonus, itemLevel, toolBonus); + } + + getEnhancementSTable(successTable: number[]): number[] { + return this._game.enhancing.getSTable(successTable); + } + + getEnhancementZTable(successTable: number[]): number[] { + return this._game.enhancing.getZScoreTable(successTable); + } + + getEnhancementCostTable(zTable: number[], sTable: number[], materialCost: number): number[] { + return this._game.enhancing.getCostTable(zTable, sTable, materialCost); + } + + getEnhancementCostTableWithProtects( + costTable: number[], + successTable: number[], + protectCost: number, + materialCost: number + ): number[] { + return this._game.enhancing.getCostWithProtects(costTable, successTable, protectCost, materialCost); + } + + getEnhancementActionsRequiredTable( + costTable: number[], + successTable: number[], + protectLevel: number, + materialCost: number + ): number[] { + return this._game.enhancing.getActionsRequiredTable(costTable, successTable, protectLevel, materialCost); + } + + getEnhancementProtectionsRequiredTable( + costTable: number[], + actionsTable: number[], + protectCost: number, + enhancementCost: number + ): number[] { + return this._game.enhancing.getProtectsRequiredTable(costTable, actionsTable, protectCost, enhancementCost); + } + + getBlessedTeaTable(successTable: number[]): number[] { + const blessedTeaBoost = + this._game.inventory.itemDetailMap[("/items/blessed_tea")].consumableDetail.buffs[0] + .flatBoost; + return this._game.enhancing.getBlessedTeaTable(successTable, blessedTeaBoost); + } + + getTotalMaterialCost(materials: { name: String | null; amount: number; value: number }[] | undefined): number { + let totalCost = 0; + if (materials) { + for (let i = 0; i < materials.length; i++) { + totalCost += materials[i].value * materials[i].amount; + } + } + return totalCost; + } + + getItemLevel(itemHrid: ItemHrid): number { + return this._game.inventory.itemDetailMap[itemHrid].itemLevel; + } + + getProtectLevel(costTable: number[], protectTable: number[]): number { + return this._game.enhancing.recommendedProtectLevel(costTable, protectTable); + } + + getEnhancingTool(): CharacterItem | null { + return this._game.inventory.getEnhancingTool(); + } + + getEnhancingToolBonus(item: CharacterItem | null): number { + if (item == null) { + return 0; + } else { + const enhancementLevel = item.enhancementLevel; + const itemDetails = this._game.inventory.itemDetailMap[item.itemHrid]; + return this._game.enhancing.getEnhancingToolBonus(enhancementLevel, itemDetails); + } + } + + getProtectionAmount( + protectionAmountTable: number[], + targetItemLevel: number, + currentEnhancementLevel: number, + useProtection: boolean + ): number { + if (useProtection) { + return protectionAmountTable[targetItemLevel] - protectionAmountTable[currentEnhancementLevel]; + } + return 0; + } + + getActionAmount( + actionAmountTable: number[], + enhancementCostTable: number[], + blessedTeaTable: number[], + targetItemLevel: number, + currentEnhancementLevel: number, + totalMaterialCost: number, + useProtection: boolean, + useBlessedTea: boolean + ): number { + let actionAmount = 0; + if (useProtection) { + actionAmount = actionAmountTable[targetItemLevel] - actionAmountTable[currentEnhancementLevel]; + } else { + actionAmount = + (enhancementCostTable[targetItemLevel] - enhancementCostTable[currentEnhancementLevel]) / + totalMaterialCost; + } + if (useBlessedTea) { + return actionAmount / blessedTeaTable[targetItemLevel]; + } + return actionAmount; + } + + getExperience( + sTable: number[], + actionAmount: number, + itemLevel: number, + currentLevel: number, + targetLevel: number, + useWisdomTea: boolean + ): number { + const wisdomTeaBuff = + 1 + + this._game.inventory.itemDetailMap[("/items/wisdom_tea")].consumableDetail.buffs[0] + .flatBoost; + let experience = 0; + const baseExpRate = EnhancingConstants.experiencePerLevel * itemLevel + EnhancingConstants.experienceBonus; + let remainingActionAmount = actionAmount; + for (let i = currentLevel + 1; i < targetLevel; i++) { + experience += + baseExpRate * i * actionAmount * sTable[i] + + baseExpRate * + i * + (remainingActionAmount - actionAmount * sTable[i]) * + (1 - sTable[i]) * + EnhancingConstants.failureExperienceRate; + remainingActionAmount = actionAmount * sTable[i]; + } + experience += baseExpRate * targetLevel; + if (remainingActionAmount - 1 > 0) { + experience += + baseExpRate * targetLevel * (remainingActionAmount - 1) * EnhancingConstants.failureExperienceRate; + } + if (useWisdomTea) { + experience *= wisdomTeaBuff; + } + return experience; + } + + getTimeTaken(actionAmount: number, enhancingLevel: number, itemLevel: number): number { + let actionSpeedBonus = 1 + this._game.skills.getSkillEfficiencyBonusRatio(itemLevel, enhancingLevel); + const baseTime = EnhancingConstants.actionSpeed; + return (baseTime / actionSpeedBonus) * actionAmount; + } + + getTimeTakenString(totalSeconds: number): String { + return DateFormatter.secondsToHHMMSS(totalSeconds); + } +} diff --git a/src/MooLite/plugins/EnhancerHelper/EnhancerHelperPluginDisplay.vue b/src/MooLite/plugins/EnhancerHelper/EnhancerHelperPluginDisplay.vue new file mode 100644 index 0000000..1cda0b2 --- /dev/null +++ b/src/MooLite/plugins/EnhancerHelper/EnhancerHelperPluginDisplay.vue @@ -0,0 +1,257 @@ + + + diff --git a/src/MooLite/util/DateFormatter.ts b/src/MooLite/util/DateFormatter.ts index c3447a2..5f3130c 100644 --- a/src/MooLite/util/DateFormatter.ts +++ b/src/MooLite/util/DateFormatter.ts @@ -2,4 +2,12 @@ export class DateFormatter { public static toHHMM(date: Date): string { return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`; } + + public static secondsToHHMMSS(totalSeconds: number): String { + const totalMinutes = Math.floor(totalSeconds / 60); + const seconds = Math.floor(totalSeconds % 60); + const hours = Math.floor(totalMinutes / 60); + const minutes = Math.floor(totalMinutes % 60); + return hours + "h " + minutes + "m " + seconds + "s"; + } } diff --git a/src/MooLite/util/Math.ts b/src/MooLite/util/Math.ts new file mode 100644 index 0000000..406bdce --- /dev/null +++ b/src/MooLite/util/Math.ts @@ -0,0 +1,16 @@ +export class Math { + public static multiplyArray(numbers: number[]): number { + let result = 1; + for (let num in numbers) { + result = result * numbers[num]; + } + return result; + } + public static sumArray(numbers: number[]): number[] { + let sumTable = []; + for (let i = 0; i < numbers.length; i++) { + sumTable.push(numbers.slice(0, i + 1).reduce((a, b) => a + b, 0)); + } + return sumTable; + } +} diff --git a/src/main.ts b/src/main.ts index 10203f9..b5142c8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,6 +17,7 @@ import { LeaderboardPlugin } from "src/MooLite/plugins/Leaderboard/LeaderboardPl import { LootNotifierPlugin } from "src/MooLite/plugins/LootNotifier/LootNotifierPlugin"; import { ConsumableNotifierPlugin } from "./MooLite/plugins/ConsumableNotifier/ConsumableNotifierPlugin"; import { EquipmentExporterPlugin } from "src/MooLite/plugins/EquipmentExporter/EquipmentExporterPlugin"; +import { EnhancerHelperPlugin } from "src/MooLite/plugins/EnhancerHelper/EnhancerHelperPlugin"; declare global { interface Window { @@ -57,6 +58,7 @@ const launchMooLite = () => { new LeaderboardPlugin(), new ConsumableNotifierPlugin(), new EquipmentExporterPlugin(), + new EnhancerHelperPlugin(), ]) as unknown as MooLitePlugin[]; const pluginManager = reactive(new PluginManager(game, plugins)) as PluginManager;