From 9e7ae5bc3fca8ee3d654aeff1c11c33bf0a0dbda Mon Sep 17 00:00:00 2001 From: GanyusLeftHorn <1244229+GanyusLeftHorn@users.noreply.github.com> Date: Fri, 5 Aug 2022 17:28:34 +0200 Subject: [PATCH] Implement Light Cone Enhancement (#52) * Implement avatar levelup and promotion. * Implement equipment enhancement features. --- src/db/Inventory.ts | 26 ++++- src/server/kcp/Session.ts | 3 - src/server/packets/AvatarExpUpCsReq.ts | 6 +- src/server/packets/ExpUpEquipmentCsReq.ts | 100 ++++++++++++++++++++ src/server/packets/LockEquipmentCsReq.ts | 26 +++++ src/server/packets/PromoteAvatarCsReq.ts | 3 - src/server/packets/PromoteEquipmentCsReq.ts | 36 +++++++ src/server/packets/RankUpEquipmentCsReq.ts | 32 +++++++ src/util/excel/EquipmentExcel.ts | 20 ++++ src/util/excel/EquipmentExpItemExcel.ts | 20 ++++ src/util/excel/EquipmentExpTypeExcel.ts | 20 ++++ src/util/excel/EquipmentPromotionExcel.ts | 20 ++++ 12 files changed, 300 insertions(+), 12 deletions(-) create mode 100644 src/server/packets/ExpUpEquipmentCsReq.ts create mode 100644 src/server/packets/LockEquipmentCsReq.ts create mode 100644 src/server/packets/PromoteEquipmentCsReq.ts create mode 100644 src/server/packets/RankUpEquipmentCsReq.ts create mode 100644 src/util/excel/EquipmentExcel.ts create mode 100644 src/util/excel/EquipmentExpItemExcel.ts create mode 100644 src/util/excel/EquipmentExpTypeExcel.ts create mode 100644 src/util/excel/EquipmentPromotionExcel.ts diff --git a/src/db/Inventory.ts b/src/db/Inventory.ts index 3aa3787..6c80380 100644 --- a/src/db/Inventory.ts +++ b/src/db/Inventory.ts @@ -119,6 +119,15 @@ export default class Inventory { return 0; } + /** + * Fetch the equipment with the given unique ID from the player's inventory. + * @param uniqueId The unique ID of the equipment to fetch. + * @returns The `Equipment` with the given unique ID, or `undefined` if the player does not have that equipment. + */ + public getEquipmentByUid(uniqueId: number) { + return this.db.equipments.filter(e => e.uniqueId == uniqueId)?.[0]; + } + /******************************************************************************** Add items to the inventory. ********************************************************************************/ @@ -376,17 +385,28 @@ export default class Inventory { /******************************************************************************** Player updating. ********************************************************************************/ - private sendMaterialUpdate() { + /** + * Send `PlayerSyncScNotify` for materials. + */ + public sendMaterialUpdate() { this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ materialList: this.getMaterialList() })); } - private sendEquipmentUpdate() { + + /** + * Send `PlayerSyncScNotify` for equipments. + */ + public sendEquipmentUpdate() { this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ equipmentList: this.getEquipmentList() })); } - private sendRelicUpdate() { + + /** + * Send `PlayerSyncScNotify` for relics. + */ + public sendRelicUpdate() { this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ relicList: this.getRelicsList() })); diff --git a/src/server/kcp/Session.ts b/src/server/kcp/Session.ts index f810f8d..ff4164a 100644 --- a/src/server/kcp/Session.ts +++ b/src/server/kcp/Session.ts @@ -80,7 +80,6 @@ export default class Session { public async sync() { const avatars = await Avatar.loadAvatarsForPlayer(this.player); - //const avatars = await Avatar.fromUID(this.player.db._id); const inventory = await this.player.getInventory(); this.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ @@ -92,8 +91,6 @@ export default class Session { relicList: inventory.getRelicsList(), basicInfo: this.player.db.basicInfo })); - - //this.player.save(); } public async send,>(type: Class, data: UnWrapMessageType) { diff --git a/src/server/packets/AvatarExpUpCsReq.ts b/src/server/packets/AvatarExpUpCsReq.ts index 8bac1b3..6f061d0 100644 --- a/src/server/packets/AvatarExpUpCsReq.ts +++ b/src/server/packets/AvatarExpUpCsReq.ts @@ -65,9 +65,9 @@ export default async function handle(session: Session, packet: Packet) { // Calculate the character's new EXP and any excess EXP. let excessExp = 0; - if (avatar.db.level == levelCap && currentAvatarExp >= nextRequiredExp) { - avatar.db.exp = nextRequiredExp; - excessExp = currentAvatarExp - nextRequiredExp; + if (avatar.db.level == levelCap) { + avatar.db.exp = 0; + excessExp = currentAvatarExp; } else { avatar.db.exp = currentAvatarExp; diff --git a/src/server/packets/ExpUpEquipmentCsReq.ts b/src/server/packets/ExpUpEquipmentCsReq.ts new file mode 100644 index 0000000..786b778 --- /dev/null +++ b/src/server/packets/ExpUpEquipmentCsReq.ts @@ -0,0 +1,100 @@ +import { ExpUpEquipmentCsReq, ExpUpEquipmentScRsp } from "../../data/proto/StarRail"; +import { PayItemData } from "../../db/Inventory"; +import EquipmentExcel from "../../util/excel/EquipmentExcel"; +import EquipmentExpItemExcel from "../../util/excel/EquipmentExpItemExcel"; +import EquipmentExpTypeExcel from "../../util/excel/EquipmentExpTypeExcel"; +import EquipmentPromotionExcel from "../../util/excel/EquipmentPromotionExcel"; +import Packet from "../kcp/Packet"; +import Session from "../kcp/Session"; + +export default async function handle(session: Session, packet: Packet) { + const body = packet.body as ExpUpEquipmentCsReq; + const inventory = await session.player.getInventory(); + + // Get the target equipment. + const equipmentId = body.equipmentUniqueId; + const equipment = inventory.getEquipmentByUid(equipmentId); + const equipmentExcelData = EquipmentExcel.fromId(equipment.tid); + + // Determine the next level cap based on the equipment's current promotion. + const levelCap = EquipmentPromotionExcel.fromId(`${equipment.tid}:${equipment.promotion}`).MaxLevel; + + // Determine the EXP we get from the consumed items, and the coins it will cost. + let exp = 0; + let cost = 0; + + const costMaterialList = []; + const costEquipmentList = [] + + for (const item of body.costData!.itemList) { + // Determine amount of EXP given by that item. + // If the consumed item is a PileItem, we can fetch the EXP it gives from the excels. + if (item.pileItem) { + const expItemData = EquipmentExpItemExcel.fromId(item.pileItem.itemId); + exp += expItemData.ExpProvide * item.pileItem.itemNum; + cost += expItemData.CoinCost; + + costMaterialList.push({ id: item.pileItem.itemId, count: item.pileItem.itemNum } as PayItemData); + } + else if (item.equipmentUniqueId) { + const consumedEquipment = inventory.getEquipmentByUid(item.equipmentUniqueId); + const consumedEquipmentExcelData = EquipmentExcel.fromId(consumedEquipment.tid); + + exp += consumedEquipmentExcelData.ExpProvide; + cost += consumedEquipmentExcelData.CoinCost; + + costEquipmentList.push(item.equipmentUniqueId); + } + } + + costMaterialList.push({ id: 2, count: cost } as PayItemData); + + // Try consuming materials. + const success = await inventory.payItems(costMaterialList); + if (!success) { + // ToDo: Correct retcode. + session.send(ExpUpEquipmentScRsp, { retcode: 1, returnItemList: [] } as ExpUpEquipmentScRsp); + return; + } + + for (const id of costEquipmentList) { + await inventory.removeEquipment(id); + } + + // Cost has been paid - now level up. + let currentEquipmentExp = equipment.exp + exp; + let nextRequiredExp = EquipmentExpTypeExcel.fromId(`${equipmentExcelData.ExpType}:${equipment.level}`).Exp; + while (currentEquipmentExp >= nextRequiredExp && equipment.level < levelCap) { + // Increase level. + equipment.level++; + + // Deduct EXP necessary for this level. + currentEquipmentExp -= nextRequiredExp; + + // Determine EXP necessary for the next level. + nextRequiredExp = EquipmentExpTypeExcel.fromId(`${equipmentExcelData.ExpType}:${equipment.level}`).Exp; + } + + // Calculate the equipment's new EXP and any excess EXP. + let excessExp = 0; + if (equipment.level == levelCap) { + equipment.exp = 0; + excessExp = currentEquipmentExp; + } + else { + equipment.exp = currentEquipmentExp; + } + + // Save. + await inventory.save(); + + // ToDo: Handle return items. + + // Done. Sync and send response. + await session.sync(); + + session.send(ExpUpEquipmentScRsp, { + retcode: 0, + returnItemList: [] + } as ExpUpEquipmentScRsp); +} \ No newline at end of file diff --git a/src/server/packets/LockEquipmentCsReq.ts b/src/server/packets/LockEquipmentCsReq.ts new file mode 100644 index 0000000..b280b49 --- /dev/null +++ b/src/server/packets/LockEquipmentCsReq.ts @@ -0,0 +1,26 @@ +import { LockEquipmentCsReq, LockEquipmentScRsp } from "../../data/proto/StarRail"; +import Packet from "../kcp/Packet"; +import Session from "../kcp/Session"; + +export default async function handle(session: Session, packet: Packet) { + const body = packet.body as LockEquipmentCsReq; + + // Fetch the equipment in question. + const inventory = await session.player.getInventory(); + const equipment = inventory.getEquipmentByUid(body.equipmentUniqueId); + + if (!equipment) { + session.send(LockEquipmentScRsp, { retcode: 1 } as LockEquipmentScRsp); + } + + // Toggle the equipment's lock. + equipment.isProtected = !equipment.isProtected; + await inventory.save(); + + // Done. Send and sync. + inventory.sendEquipmentUpdate(); + session.send(LockEquipmentScRsp, { + retcode: 0, + equipmentUniqueId: body.equipmentUniqueId + } as LockEquipmentScRsp); +} \ No newline at end of file diff --git a/src/server/packets/PromoteAvatarCsReq.ts b/src/server/packets/PromoteAvatarCsReq.ts index dfb8a30..098bdfa 100644 --- a/src/server/packets/PromoteAvatarCsReq.ts +++ b/src/server/packets/PromoteAvatarCsReq.ts @@ -1,10 +1,7 @@ import { PromoteAvatarCsReq, PromoteAvatarScRsp } from "../../data/proto/StarRail"; import Avatar from "../../db/Avatar"; import { PayItemData } from "../../db/Inventory"; -import AvatarExcel from "../../util/excel/AvatarExcel"; -import AvatarExpItemExcel from "../../util/excel/AvatarExpItemExcel"; import AvatarPromotionExcel from "../../util/excel/AvatarPromotionExcel"; -import ExpTypeExcel from "../../util/excel/ExpTypeExcel"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; diff --git a/src/server/packets/PromoteEquipmentCsReq.ts b/src/server/packets/PromoteEquipmentCsReq.ts new file mode 100644 index 0000000..219c2cb --- /dev/null +++ b/src/server/packets/PromoteEquipmentCsReq.ts @@ -0,0 +1,36 @@ +import { PromoteEquipmentCsReq, PromoteEquipmentScRsp } from "../../data/proto/StarRail"; +import { PayItemData } from "../../db/Inventory"; +import EquipmentPromotionExcel from "../../util/excel/EquipmentPromotionExcel"; +import Packet from "../kcp/Packet"; +import Session from "../kcp/Session"; + +export default async function handle(session: Session, packet: Packet) { + const body = packet.body as PromoteEquipmentCsReq; + const inventory = await session.player.getInventory(); + + // Get the target avatar. + const equipmentId = body.equipmentUniqueId; + const equipment = inventory.getEquipmentByUid(equipmentId); + const promotionExcelData = EquipmentPromotionExcel.fromId(`${equipment.tid}:${equipment.promotion}`); + + // Build list of consumed items. We take this from the excel, instead of the Req. + const costMaterialList = promotionExcelData.PromotionCostList.map(c => { return { id: c.ItemID, count: c.ItemNum } as PayItemData }); + + // Try consuming materials. + const success = await inventory.payItems(costMaterialList); + if (!success) { + // ToDo: Correct retcode. + session.send(PromoteEquipmentScRsp, { retcode: 1 } as PromoteEquipmentScRsp); + return; + } + + await inventory.save(); + + // Promote the avatar and save. + equipment.promotion++; + await inventory.save(); + + // Done. Sync and send response. + await session.sync(); + session.send(PromoteEquipmentScRsp, { retcode: 0 } as PromoteEquipmentScRsp); +} \ No newline at end of file diff --git a/src/server/packets/RankUpEquipmentCsReq.ts b/src/server/packets/RankUpEquipmentCsReq.ts new file mode 100644 index 0000000..2a701a0 --- /dev/null +++ b/src/server/packets/RankUpEquipmentCsReq.ts @@ -0,0 +1,32 @@ +import { RankUpEquipmentCsReq, RankUpEquipmentScRsp } from "../../data/proto/StarRail"; +import Packet from "../kcp/Packet"; +import Session from "../kcp/Session"; + +export default async function handle(session: Session, packet: Packet) { + const body = packet.body as RankUpEquipmentCsReq; + const inventory = await session.player.getInventory(); + + // Get equipment. + const equipment = inventory.getEquipmentByUid(body.equipmentUniqueId); + + // Check if all sacrificed equipments exist. + for (const id of body.equipmentIdList) { + if (!inventory.getEquipmentByUid(id)) { + session.send(RankUpEquipmentScRsp, { retcode: 1 } as RankUpEquipmentScRsp); + return; + } + } + + // Remove sacrificed equipments. + for (const id of body.equipmentIdList) { + await inventory.removeEquipment(id); + } + + // Increase rank and save. + equipment.rank += body.equipmentIdList.length; + await inventory.save(); + + // Done. Send and sync. + await session.sync(); + session.send(RankUpEquipmentScRsp, { retcode: 0 } as RankUpEquipmentScRsp); +} \ No newline at end of file diff --git a/src/util/excel/EquipmentExcel.ts b/src/util/excel/EquipmentExcel.ts new file mode 100644 index 0000000..5a12acf --- /dev/null +++ b/src/util/excel/EquipmentExcel.ts @@ -0,0 +1,20 @@ +import _EquipmentExcelTable from "../../data/excel/EquipmentExcelTable.json"; +type EquipmentExcelTableEntry = typeof _EquipmentExcelTable[keyof typeof _EquipmentExcelTable] +const EquipmentExcelTable = _EquipmentExcelTable as { [key: string]: EquipmentExcelTableEntry }; + +export default class EquipmentExcel { + private constructor() { + } + + public static all() : EquipmentExcelTableEntry[] { + return Object.values(EquipmentExcelTable); + } + + public static fromId(id: number) : EquipmentExcelTableEntry { + return EquipmentExcelTable[id]; + } + + public static fromIds(ids: number[]): EquipmentExcelTableEntry[] { + return ids.map(id => EquipmentExcel.fromId(id)); + } +} \ No newline at end of file diff --git a/src/util/excel/EquipmentExpItemExcel.ts b/src/util/excel/EquipmentExpItemExcel.ts new file mode 100644 index 0000000..6c01578 --- /dev/null +++ b/src/util/excel/EquipmentExpItemExcel.ts @@ -0,0 +1,20 @@ +import _EquipmentExpItemExcelTable from "../../data/excel/EquipmentExpItemExcelTable.json"; +type EquipmentExpItemExcelTableEntry = typeof _EquipmentExpItemExcelTable[keyof typeof _EquipmentExpItemExcelTable] +const EquipmentExpItemExcelTable = _EquipmentExpItemExcelTable as { [key: string]: EquipmentExpItemExcelTableEntry }; + +export default class EquipmentExpItemExcel { + private constructor() { + } + + public static all() : EquipmentExpItemExcelTableEntry[] { + return Object.values(EquipmentExpItemExcelTable); + } + + public static fromId(id: number) : EquipmentExpItemExcelTableEntry { + return EquipmentExpItemExcelTable[id]; + } + + public static fromIds(ids: number[]): EquipmentExpItemExcelTableEntry[] { + return ids.map(id => EquipmentExpItemExcel.fromId(id)); + } +} \ No newline at end of file diff --git a/src/util/excel/EquipmentExpTypeExcel.ts b/src/util/excel/EquipmentExpTypeExcel.ts new file mode 100644 index 0000000..febea53 --- /dev/null +++ b/src/util/excel/EquipmentExpTypeExcel.ts @@ -0,0 +1,20 @@ +import _EquipmentExpTypeExcelTable from "../../data/excel/EquipmentExpTypeExcelTable.json"; +type EquipmentExpTypeExcelTableEntry = typeof _EquipmentExpTypeExcelTable[keyof typeof _EquipmentExpTypeExcelTable] +const EquipmentExpTypeExcelTable = _EquipmentExpTypeExcelTable as { [key: string]: EquipmentExpTypeExcelTableEntry }; + +export default class EquipmentExpTypeExcel { + private constructor() { + } + + public static all() : EquipmentExpTypeExcelTableEntry[] { + return Object.values(EquipmentExpTypeExcelTable); + } + + public static fromId(id: string) : EquipmentExpTypeExcelTableEntry { + return EquipmentExpTypeExcelTable[id]; + } + + public static fromIds(ids: string[]): EquipmentExpTypeExcelTableEntry[] { + return ids.map(id => EquipmentExpTypeExcel.fromId(id)); + } +} \ No newline at end of file diff --git a/src/util/excel/EquipmentPromotionExcel.ts b/src/util/excel/EquipmentPromotionExcel.ts new file mode 100644 index 0000000..c1500d1 --- /dev/null +++ b/src/util/excel/EquipmentPromotionExcel.ts @@ -0,0 +1,20 @@ +import _EquipmentPromotionExcelTable from "../../data/excel/EquipmentPromotionExcelTable.json"; +type EquipmentPromotionExcelTableEntry = typeof _EquipmentPromotionExcelTable[keyof typeof _EquipmentPromotionExcelTable] +const EquipmentPromotionExcelTable = _EquipmentPromotionExcelTable as { [key: string]: EquipmentPromotionExcelTableEntry }; + +export default class EquipmentPromotionExcel { + private constructor() { + } + + public static all() : EquipmentPromotionExcelTableEntry[] { + return Object.values(EquipmentPromotionExcelTable); + } + + public static fromId(id: string) : EquipmentPromotionExcelTableEntry { + return EquipmentPromotionExcelTable[id]; + } + + public static fromIds(ids: string[]): EquipmentPromotionExcelTableEntry[] { + return ids.map(id => EquipmentPromotionExcel.fromId(id)); + } +} \ No newline at end of file