From 2fe4f6a2ea989f73e0d783fde3e2a7d1f5dc566d Mon Sep 17 00:00:00 2001 From: GanyusLeftHorn <1244229+GanyusLeftHorn@users.noreply.github.com> Date: Fri, 5 Aug 2022 05:28:46 +0200 Subject: [PATCH] Implement Character Level-up and Ascension (#48) --- src/commands/item.ts | 14 ++-- src/db/Avatar.ts | 17 +++-- src/db/Inventory.ts | 21 +++++- src/db/Player.ts | 6 +- src/server/packets/AvatarExpUpCsReq.ts | 88 ++++++++++++++++++++++++ src/server/packets/PromoteAvatarCsReq.ts | 40 +++++++++++ src/util/excel/AvatarExpItemExcel.ts | 20 ++++++ src/util/excel/AvatarPromotionExcel.ts | 20 ++++++ src/util/excel/ExpTypeExcel.ts | 20 ++++++ 9 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 src/server/packets/AvatarExpUpCsReq.ts create mode 100644 src/server/packets/PromoteAvatarCsReq.ts create mode 100644 src/util/excel/AvatarExpItemExcel.ts create mode 100644 src/util/excel/AvatarPromotionExcel.ts create mode 100644 src/util/excel/ExpTypeExcel.ts diff --git a/src/commands/item.ts b/src/commands/item.ts index 39fcd14..7d41bd5 100644 --- a/src/commands/item.ts +++ b/src/commands/item.ts @@ -17,8 +17,8 @@ export default async function handle(command: Command) { let count: number = 1; let level: number = 1; - let rank: number = 1; - let promotion: number = 1; + let rank: number = 0; + let promotion: number = 0; for (let i = 2; i < command.args.length; i++) { const arg = command.args[i]; @@ -52,6 +52,9 @@ export default async function handle(command: Command) { break; } } + + // Sync session. + await player.session.sync(); } async function handleGive(player: Player, itemId: number, count:number, level: number, rank: number, promotion: number) { @@ -96,9 +99,12 @@ async function handleGiveAll(player: Player) { const inventory = await player.getInventory(); for (const entry of ItemExcel.all()) { - const count = entry.ItemType == "Material" ? 100 : 1; + const count = + (entry.ItemType == "Material") ? 1000 : + (entry.ItemType == "Virtual") ? 10_000_000 : + 1; await inventory.addItem(entry.ID, count); } - + c.log(`All materials added to ${player.uid}`); } \ No newline at end of file diff --git a/src/db/Avatar.ts b/src/db/Avatar.ts index 363ebe0..d6dddfc 100644 --- a/src/db/Avatar.ts +++ b/src/db/Avatar.ts @@ -49,9 +49,9 @@ export default class Avatar { baseAvatarId: 1001, avatarType: AvatarType.AVATAR_FORMAL_TYPE, level: 1, - exp: 1, - promotion: 1, - rank: 1, + exp: 0, + promotion: 0, + rank: 0, equipmentUniqueId: 20003, equipRelicList: [], skilltreeList: [], @@ -111,9 +111,9 @@ export default class Avatar { baseAvatarId: baseAvatarId, avatarType: AvatarType.AVATAR_FORMAL_TYPE, level: 1, - exp: 1, - promotion: 1, - rank: 1, + exp: 0, + promotion: 0, + rank: 0, equipmentUniqueId: 20003, equipRelicList: [], skilltreeList: [], @@ -144,6 +144,11 @@ export default class Avatar { return res; } + public async save() { + const db = Database.getInstance(); + await db.update("avatars", { ownerUid: this.player.uid, baseAvatarId: this.db.baseAvatarId }, this.db); + } + /******************************************************************************** Get avatar info. ********************************************************************************/ diff --git a/src/db/Inventory.ts b/src/db/Inventory.ts index 696de14..3aa3787 100644 --- a/src/db/Inventory.ts +++ b/src/db/Inventory.ts @@ -101,13 +101,24 @@ export default class Inventory { } switch (itemData.ItemType) { - case "Virtual": return 0; // ToDo: Handle virtual items. + case "Virtual": return this.getVirtualItemCount(id); case "Material": return this.db.materials[id] ?? 0; } return 0; } + private getVirtualItemCount(id: number) : number { + // ToDo: Figure out which virtual item ID is what. + switch (id) { + case 2: + return this.player.db.basicInfo.scoin; + break; + } + + return 0; + } + /******************************************************************************** Add items to the inventory. ********************************************************************************/ @@ -151,6 +162,14 @@ export default class Inventory { */ public async addVirtualItem(id: number, count: number) { // ToDo: Figure out which virtual item ID is what. + switch (id) { + case 2: + this.player.db.basicInfo.scoin += count; + break; + } + + // Save. + this.player.save(); } /** diff --git a/src/db/Player.ts b/src/db/Player.ts index 1d1c560..62f6efc 100644 --- a/src/db/Player.ts +++ b/src/db/Player.ts @@ -137,13 +137,13 @@ export default class Player { heroBasicType: HeroBasicType.BoyWarrior, basicInfo: { exp: 0, - level: 1, + level: 70, hcoin: 0, mcoin: 0, nickname: acc.name, scoin: 0, - stamina: 100, - worldLevel: 1, + stamina: 180, + worldLevel: 6, }, lineup: { curIndex: 0, diff --git a/src/server/packets/AvatarExpUpCsReq.ts b/src/server/packets/AvatarExpUpCsReq.ts new file mode 100644 index 0000000..8bac1b3 --- /dev/null +++ b/src/server/packets/AvatarExpUpCsReq.ts @@ -0,0 +1,88 @@ +import { AvatarExpUpCsReq, AvatarExpUpScRsp } 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"; + +export default async function handle(session: Session, packet: Packet) { + const body = packet.body as AvatarExpUpCsReq; + const inventory = await session.player.getInventory(); + + // Get the target avatar. + const avatarId = body.baseAvatarId; + const avatar = await Avatar.loadAvatarForPlayer(session.player, avatarId); + const avatarExcelData = AvatarExcel.fromId(avatarId); + + // Determine the next level cap based on the avatar's current promotion. + const levelCap = AvatarPromotionExcel.fromId(`${avatarId}:${avatar.db.promotion}`).MaxLevel; + + // Determine the EXP we get from the consumed items. + let exp = 0; + const costMaterialList = []; + for (const item of body.itemCost!.itemList) { + // Determine amount of EXP given by that item. + // We know that the cost items given in this Req will be `PileItem`s. + const expPerItem = AvatarExpItemExcel.fromId(item.pileItem!.itemId).Exp; + + // Add EXP for the number of items consumed. + exp += expPerItem * item.pileItem!.itemNum; + + // Add material to cost. + costMaterialList.push({ id: item.pileItem!.itemId, count: item.pileItem!.itemNum } as PayItemData); + } + + // Determine cost, which is always 10% of EXP, and add to the list of cost materials. + const coinCost = exp * 0.1; + costMaterialList.push({ id: 2, count: coinCost } as PayItemData); + + // Try consuming materials. + const success = await inventory.payItems(costMaterialList); + if (!success) { + // ToDo: Correct retcode. + session.send(AvatarExpUpScRsp, { retcode: 1, returnItemList: [] } as AvatarExpUpScRsp); + return; + } + + await inventory.save(); + + // Cost has been paid - now level up. + let currentAvatarExp = avatar.db.exp + exp; + let nextRequiredExp = ExpTypeExcel.fromId(`${avatarExcelData.ExpGroup}:${avatar.db.level}`).Exp; + while (currentAvatarExp >= nextRequiredExp && avatar.db.level < levelCap) { + // Increase level. + avatar.db.level++; + + // Deduct EXP necessary for this level. + currentAvatarExp -= nextRequiredExp; + + // Determine EXP necessary for the next level. + nextRequiredExp = ExpTypeExcel.fromId(`${avatarExcelData.ExpGroup}:${avatar.db.level}`).Exp; + } + + // 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; + } + else { + avatar.db.exp = currentAvatarExp; + } + + // Save. + await avatar.save(); + + // ToDo: Handle return items. + + // Done. Sync and send response. + await session.sync(); + + session.send(AvatarExpUpScRsp, { + retcode: 0, + returnItemList: [] + } as AvatarExpUpScRsp); +} \ No newline at end of file diff --git a/src/server/packets/PromoteAvatarCsReq.ts b/src/server/packets/PromoteAvatarCsReq.ts new file mode 100644 index 0000000..dfb8a30 --- /dev/null +++ b/src/server/packets/PromoteAvatarCsReq.ts @@ -0,0 +1,40 @@ +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"; + +export default async function handle(session: Session, packet: Packet) { + const body = packet.body as PromoteAvatarCsReq; + const inventory = await session.player.getInventory(); + + // Get the target avatar. + const avatarId = body.baseAvatarId; + const avatar = await Avatar.loadAvatarForPlayer(session.player, avatarId); + const promotionExcelData = AvatarPromotionExcel.fromId(`${avatarId}:${avatar.db.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(PromoteAvatarScRsp, { retcode: 1 } as PromoteAvatarScRsp); + return; + } + + await inventory.save(); + + // Promote the avatar and save. + avatar.db.promotion++; + await avatar.save(); + + // Done. Sync and send response. + await session.sync(); + session.send(PromoteAvatarScRsp, { retcode: 0 } as PromoteAvatarScRsp); +} \ No newline at end of file diff --git a/src/util/excel/AvatarExpItemExcel.ts b/src/util/excel/AvatarExpItemExcel.ts new file mode 100644 index 0000000..27d1036 --- /dev/null +++ b/src/util/excel/AvatarExpItemExcel.ts @@ -0,0 +1,20 @@ +import _AvatarExpItemConfigExcelTable from "../../data/excel/AvatarExpItemConfigExcelTable.json"; +type AvatarExpItemConfigExcelTableEntry = typeof _AvatarExpItemConfigExcelTable[keyof typeof _AvatarExpItemConfigExcelTable] +const AvatarExpItemConfigExcelTable = _AvatarExpItemConfigExcelTable as { [key: string]: AvatarExpItemConfigExcelTableEntry }; + +export default class AvatarExpItemExcel { + private constructor() { + } + + public static all() : AvatarExpItemConfigExcelTableEntry[] { + return Object.values(AvatarExpItemConfigExcelTable); + } + + public static fromId(id: number) : AvatarExpItemConfigExcelTableEntry { + return AvatarExpItemConfigExcelTable[id]; + } + + public static fromIds(ids: number[]): AvatarExpItemConfigExcelTableEntry[] { + return ids.map(id => AvatarExpItemExcel.fromId(id)); + } +} \ No newline at end of file diff --git a/src/util/excel/AvatarPromotionExcel.ts b/src/util/excel/AvatarPromotionExcel.ts new file mode 100644 index 0000000..18c1bf5 --- /dev/null +++ b/src/util/excel/AvatarPromotionExcel.ts @@ -0,0 +1,20 @@ +import _AvatarPromotionExcelTable from "../../data/excel/AvatarPromotionExcelTable.json"; +type AvatarPromotionExcelTableEntry = typeof _AvatarPromotionExcelTable[keyof typeof _AvatarPromotionExcelTable] +const AvatarPromotionExcelTable = _AvatarPromotionExcelTable as { [key: string]: AvatarPromotionExcelTableEntry }; + +export default class AvatarPromotionExcel { + private constructor() { + } + + public static all() : AvatarPromotionExcelTableEntry[] { + return Object.values(AvatarPromotionExcelTable); + } + + public static fromId(id: string) : AvatarPromotionExcelTableEntry { + return AvatarPromotionExcelTable[id]; + } + + public static fromIds(ids: string[]): AvatarPromotionExcelTableEntry[] { + return ids.map(id => AvatarPromotionExcel.fromId(id)); + } +} \ No newline at end of file diff --git a/src/util/excel/ExpTypeExcel.ts b/src/util/excel/ExpTypeExcel.ts new file mode 100644 index 0000000..02591f0 --- /dev/null +++ b/src/util/excel/ExpTypeExcel.ts @@ -0,0 +1,20 @@ +import _ExpTypeExcelTable from "../../data/excel/ExpTypeExcelTable.json"; +type ExpTypeExcelTableEntry = typeof _ExpTypeExcelTable[keyof typeof _ExpTypeExcelTable] +const ExpTypeExcelTable = _ExpTypeExcelTable as { [key: string]: ExpTypeExcelTableEntry }; + +export default class ExpTypeExcel { + private constructor() { + } + + public static all() : ExpTypeExcelTableEntry[] { + return Object.values(ExpTypeExcelTable); + } + + public static fromId(id: string) : ExpTypeExcelTableEntry { + return ExpTypeExcelTable[id]; + } + + public static fromIds(ids: string[]): ExpTypeExcelTableEntry[] { + return ids.map(id => ExpTypeExcel.fromId(id)); + } +} \ No newline at end of file