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/commands/scene.ts b/src/commands/scene.ts index ed42395..8f95087 100644 --- a/src/commands/scene.ts +++ b/src/commands/scene.ts @@ -20,11 +20,9 @@ export default async function handle(command: Command) { if (!planeID) return c.log("Usage: /scene "); - Interface.target.player.db.posData = { - floorID: planeID.StartFloorID, - planeID: planeID.PlaneID, - pos: Interface.target.player.db.posData.pos - }; + // Update scene information on player. + Interface.target.player.db.posData.planeID = planeID!.PlaneID; + Interface.target.player.db.posData.floorID = planeID!.StartFloorID; await Interface.target.player.save() //ty for tamilpp25 scene diff --git a/src/db/Avatar.ts b/src/db/Avatar.ts index d68a772..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: [], 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 7b054e3..62f6efc 100644 --- a/src/db/Player.ts +++ b/src/db/Player.ts @@ -43,7 +43,11 @@ interface PlayerI { posData: { floorID: number; planeID: number; - pos: Vector; + pos: { + x: number, + y: number, + z: number + }; } } @@ -52,7 +56,7 @@ export default class Player { public readonly scene: Scene; private inventory!: Inventory; - private constructor(readonly session: Session, public db: PlayerI) { + private constructor(readonly session: Session, public readonly db: PlayerI) { this.uid = db._id; this.scene = new Scene(this); } @@ -133,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, @@ -155,34 +159,25 @@ export default class Player { } }, banned: false - } as PlayerI - - const baseLineup = { - avatarList: [1001], - extraLineupType: ExtraLineupType.LINEUP_NONE, - index: 0, - isVirtual: false, - leaderSlot: 0, - mp: 100, // ?? Not sure what this is - name: "", - planeId: 10001 - } + } as PlayerI; const LINEUPS = 6; for (let i = 0; i < LINEUPS; i++) { - const copy = { - ...baseLineup, + const l : LineupI = { + avatarList: [1001], + extraLineupType: ExtraLineupType.LINEUP_NONE, index: i, - name: `Team ${i}` + isVirtual: false, + leaderSlot: 0, + mp: 100, + name: `Team ${i}`, + planeId: 10001 }; - dataObj.lineup.lineups[i] = copy; + dataObj.lineup.lineups[i] = l; } const player = new Player(session, dataObj); - await Avatar.addAvatarToPlayer(player, 1001); - // await Avatar.create(uid, 1001, 0); - // Save to database and return. await db.set("players", dataObj); 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/DressAvatarCsReq.ts b/src/server/packets/DressAvatarCsReq.ts index d804bba..7d72746 100644 --- a/src/server/packets/DressAvatarCsReq.ts +++ b/src/server/packets/DressAvatarCsReq.ts @@ -6,7 +6,7 @@ import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { const body = packet.body as DressAvatarCsReq; - let avatar = await Avatar.loadAvatarForPlayer(session.player, body.baseAvatarId); + const avatar = await Avatar.loadAvatarForPlayer(session.player, body.baseAvatarId); avatar.db.equipmentUniqueId = body.equipmentUniqueId; await avatar.save(); diff --git a/src/server/packets/JoinLineupCsReq.ts b/src/server/packets/JoinLineupCsReq.ts index 0c72416..c394b50 100644 --- a/src/server/packets/JoinLineupCsReq.ts +++ b/src/server/packets/JoinLineupCsReq.ts @@ -11,31 +11,6 @@ export default async function handle(session: Session, packet: Packet) { // Replace avatar in the player's lineup. const slot = body.slot ?? 0; session.player.db.lineup.lineups[session.player.db.lineup.curIndex].avatarList[slot] = body.baseAvatarId; - - /* - - let lineup = await session.player.getLineup(); - const avatarList = []; - - // What in the fuck is the purpose of this loop supposed to be?! - for (const avatarId in lineup) { - const avatar = await Avatar.fromUID(session.player.db._id, Number(avatarId)); - if (avatar.length === 0) return session.c.warn(`Avatar ${body.baseAvatarId} not found`); - if (avatar) avatarList.push(avatar[0]); - } - - lineup.avatarList[slot] = { - avatarType: AvatarType.AVATAR_FORMAL_TYPE, - hp: 10000, - id: body.baseAvatarId, - satiety: 100, - slot, - sp: 10000 - }; - if (body.extraLineupType) lineup.extraLineupType = body.extraLineupType; - session.player.setLineup(lineup); - */ - await session.player.save(); session.send(SyncLineupNotify, { 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/server/packets/RankUpAvatarCsReq.ts b/src/server/packets/RankUpAvatarCsReq.ts index 0aac5f6..d6093c9 100644 --- a/src/server/packets/RankUpAvatarCsReq.ts +++ b/src/server/packets/RankUpAvatarCsReq.ts @@ -1,5 +1,6 @@ import { RankUpAvatarCsReq, RankUpAvatarScRsp } from "../../data/proto/StarRail"; import Avatar from "../../db/Avatar"; +import { PayItemData } from "../../db/Inventory"; import Logger from "../../util/Logger"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; @@ -7,33 +8,37 @@ const c = new Logger("RankUpAvatarCsReq"); export default async function handle(session: Session, packet: Packet) { const body = packet.body as RankUpAvatarCsReq; + const dataObj: RankUpAvatarScRsp = { + retcode: 0 + }; - let retcode = 0; - if (body.costData) { + try { const inv = await session.player.getInventory(); + if (!body.costData) return; + const list = body.costData.itemList; + const arr: Array = []; - for (const item of body.costData.itemList) { - if (!item.pileItem) return; - - const succ = await inv.payItems([{ + for (let i = 0; i < list.length; i++) { + const item = list[i]; + if (!item.pileItem) continue; + arr.push({ count: item.pileItem.itemNum, - id: item.pileItem.itemId, - }]); - - if (!succ) { - retcode = 1301; - c.debug(`failed to pay item ${item.pileItem.itemId}`, item.pileItem); - break; - } + id: item.pileItem.itemId + }); } - } - if (retcode === 0) { - const avatar = await Avatar.loadAvatarForPlayer(session.player, body.baseAvatarId); - avatar.db.rank = body.rank // gidra moment: ez hack - await avatar.save(); + if (await inv.payItems(arr)) { + const avatar = await Avatar.loadAvatarForPlayer(session.player, body.baseAvatarId); + avatar.db.rank = body.rank // gidra moment: ez hack + await avatar.save(); + } else { + dataObj.retcode = 1301; + } + } catch (e) { + c.error(e as Error); + dataObj.retcode = 2; + } finally { + session.send(RankUpAvatarScRsp, dataObj); + session.sync(); } - - session.send(RankUpAvatarScRsp, { retcode }); - session.sync(); } \ No newline at end of file diff --git a/src/server/packets/SceneEntityMoveCsReq.ts b/src/server/packets/SceneEntityMoveCsReq.ts index dfbe65e..c222884 100644 --- a/src/server/packets/SceneEntityMoveCsReq.ts +++ b/src/server/packets/SceneEntityMoveCsReq.ts @@ -23,7 +23,11 @@ export default async function handle(session: Session, packet: Packet) { entity.pos = motion.pos; if (entity instanceof ActorEntity) { entity.mapLayer = entityMotion.mapLayer; - session.player.db.posData.pos = motion.pos!; + session.player.db.posData.pos = { + x: motion.pos.x, + y: motion.pos.y, + z: motion.pos.z + }; } } diff --git a/src/server/packets/StartChallengeCsReq.ts b/src/server/packets/StartChallengeCsReq.ts index 7c8c5d7..5b7bb92 100644 --- a/src/server/packets/StartChallengeCsReq.ts +++ b/src/server/packets/StartChallengeCsReq.ts @@ -17,14 +17,14 @@ export default async function handle(session: Session, packet: Packet) { rounds: 1, status: ChallengeStatus.CHALLENGE_DOING, killMonsterList: [{ - monsterId: 1001010, + monsterId: 8013010, killNum: 1, }] }, maze: { // ? Data from MappingInfoExcelTable id: 30101, - mapEntryId: 1000, + mapEntryId: 10001, floor: { floorId: 20121001, scene: { diff --git a/src/util/Banner.ts b/src/util/Banner.ts index 0cc9e99..98a1b14 100644 --- a/src/util/Banner.ts +++ b/src/util/Banner.ts @@ -1,6 +1,7 @@ import fs from 'fs'; -import { resolve } from 'path'; +import {resolve} from 'path'; import Logger from './Logger'; + const c = new Logger("Banner"); type Banner = { @@ -18,15 +19,21 @@ function r(...args: string[]) { export default class Banners { public static config: Banner[]; - public static init(){ + public static init() { Banners.readConfig(); } - private static readConfig(){ + private static readConfig() { let config: Banner[]; - const defaultConfig: Banner[] = [ - { - gachaId: 1001, + + const defaultConfig: Banner[] = []; + + // TODO: figure where is GachaBasicInfoConfigExcelTable. Temporary hardcode + const bannersID = [1001, 2001, 2002, 3001, 3002, 4001] + + for (let i = 0; i < bannersID.length; i++) { + defaultConfig.push({ + gachaId: bannersID[i], detailWebview: "", rateUpItems4: [ 1001, 1103 @@ -35,13 +42,13 @@ export default class Banners { 1102 ], costItemId: 101 // Star Rail Pass - } as Banner - ]; + } as Banner) + } try { config = JSON.parse(fs.readFileSync(r('../../banners.json')).toString()); - - for(const [index, gachaBanner] of Object.entries(config)){ + + for (const [index, gachaBanner] of Object.entries(config)) { const missing = Object.keys(defaultConfig[0]).filter(key => !gachaBanner.hasOwnProperty(key)); if (missing.length > 0) { c.log(`Missing ${missing.join(', ')}, using default values.`); @@ -54,7 +61,7 @@ export default class Banners { Banners.updateConfig(defaultConfig); } } - + private static updateConfig(config: Banner[]) { this.config = config; fs.writeFileSync(r('../../banners.json'), JSON.stringify(config, null, 2)); 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