Basic Inventory Management, /give Command, Gacha Cost (#29)

* Basic inventory management, materials.

* Add await to player save.

* Adding equipment and relics.

* Added method for paying items, add payment to gacha.

* Remove database log.

* Simplyfy give command
This commit is contained in:
GanyusLeftHorn 2022-08-03 15:16:09 +02:00 committed by GitHub
parent e24ff7d25d
commit 719b3200be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 529 additions and 6 deletions

104
src/commands/item.ts Normal file
View File

@ -0,0 +1,104 @@
import { Equipment } from "../data/proto/StarRail";
import Player from "../db/Player";
import ItemExcel from "../util/excel/ItemExcel";
import Logger from "../util/Logger";
import Interface, { Command } from "./Interface";
const c = new Logger("/item", "blue");
export default async function handle(command: Command) {
if (!Interface.target) {
c.log("No target specified");
return;
}
const player = Interface.target.player;
const actionType = command.args[0];
const itemId = Number(command.args[1]);
let count: number = 1;
let level: number = 1;
let rank: number = 1;
let promotion: number = 1;
for (let i = 2; i < command.args.length; i++) {
const arg = command.args[i];
const number = Number(command.args[i].substring(1));
if (arg.startsWith("x")) {
count = number;
}
else if (arg.startsWith("l")) {
level = number;
}
else if (arg.startsWith("r")) {
rank = number;
}
else if (arg.startsWith("p")) {
promotion = number;
}
}
switch (actionType) {
case "give": {
await handleGive(player, itemId, count, level, rank, promotion);
break;
}
case "giveall": {
await handleGiveAll(player);
break;
}
default: {
c.log(`Usage: /item <give|giveall> <itemId> [x<count>|l<level>|r<rank>|p<promotion>]*`);
break;
}
}
}
async function handleGive(player: Player, itemId: number, count:number, level: number, rank: number, promotion: number) {
if (!itemId) {
return c.log("No avatarId specified");
}
// Check if this item exists.
const itemData = ItemExcel.fromId(itemId);
if (!itemData) {
return c.log(`Item ID ${itemId} does not exist.`);
}
const inventory = await player.getInventory();
switch (itemData.ItemType) {
case "Material":
await inventory.addMaterial(itemId, count);
break;
case "Equipment":
for (let i = 0; i < count; i++) {
await inventory.addEquipment({
tid: itemId,
uniqueId: 0,
level: level,
rank: rank,
exp: 1,
isProtected: false,
promotion: promotion,
baseAvatarId: 0
} as Equipment);
}
break;
default:
return c.log(`Unsupported item type: ${itemData.ItemType}.`);
break;
}
c.log(`Added ${count} of item ${itemId} to player ${player.uid}`);
}
async function handleGiveAll(player: Player) {
const inventory = await player.getInventory();
for (const entry of ItemExcel.all()) {
const count = entry.ItemType == "Material" ? 100 : 1;
await inventory.addItem(entry.ID, count);
}
c.log(`All materials added to ${player.uid}`);
}

369
src/db/Inventory.ts Normal file
View File

@ -0,0 +1,369 @@
import { Equipment, Item, Material, PlayerSyncScNotify, Relic } from "../data/proto/StarRail";
import Logger from "../util/Logger";
import Database from "./Database";
import Player from "./Player";
import ItemExcel from "../util/excel/ItemExcel";
import { Timestamp } from "mongodb";
const c = new Logger("Inventory");
export interface PayItemData {
id: number,
count: number
}
interface InventoryI {
_id: number,
nextItemUid: number,
materials: { [key: number]: number },
relics: Relic[],
equipments: Equipment[]
}
export default class Inventory {
public readonly player : Player;
public readonly db: InventoryI;
private constructor(player: Player, db: InventoryI) {
this.player = player;
this.db = db;
}
public static async loadOrCreate(player: Player) : Promise<Inventory> {
// Try to load the player's inventory from the database.
const db = Database.getInstance();
const inventory = await db.get("inventory", { _id: player.uid }) as unknown as InventoryI; // How to get rid of this ugly fuck?!
// If successfull, we are done.
if (inventory) {
return new Inventory(player, inventory);
}
// Otherwise, we create a default inventory.
const data : InventoryI = {
_id: player.uid,
nextItemUid: 1,
materials: {},
relics: [],
equipments: []
};
await db.set("inventory", data);
return new Inventory(player, data);
}
public async save() {
const db = Database.getInstance();
await db.update("inventory", { _id: this.db._id }, this.db);
}
/********************************************************************************
Get inventory info.
********************************************************************************/
/**
* Get list of all `Material`s as proto.
* @returns List of materials.
*/
public getMaterialList() : Material[] {
const res: Material[] = [];
Object.keys(this.db.materials).forEach(key => {
res.push({ tid: Number(key), num: this.db.materials[Number(key)] } as Material);
});
return res;
}
/**
* Get list of all `Equipment`s as proto.
* @returns List of equipments.
*/
public getEquipmentList() : Equipment[] {
return this.db.equipments;
}
/**
* Get list of all `Relic`s as proto.
* @returns List of relics.
*/
public getRelicsList() : Relic[] {
return this.db.relics;
}
/**
* Returns the count of the given item (material or virtual) in the player's inventory.
* @param id The item id.
* @returns The count in the player's inventory.
*/
public async getItemCount(id: number) : Promise<number> {
// Get item data.
const itemData = ItemExcel.fromId(id);
if (!itemData) {
return 0;
}
switch (itemData.ItemType) {
case "Virtual": return 0; // ToDo: Handle virtual items.
case "Material": return this.db.materials[id] ?? 0;
}
return 0;
}
/********************************************************************************
Add items to the inventory.
********************************************************************************/
/**
* Add the given amount of the given item to the player's inventory.
* @param id The item id. For equipment and relics, this is the base id.
* @param count The amount of items to add.
*/
public async addItem(id: number, count: number) {
// Get info for the particular item we are trying to add.
const itemData = ItemExcel.fromId(id);
if (!itemData) {
return;
}
// Handle adding depending on item type.
const t = itemData.ItemType;
if (t == "Virtual") {
await this.addVirtualItem(id, count);
}
else if (t == "Material") {
await this.addMaterial(id, count);
}
else if (t == "Equipment") {
for (let i = 0; i < count; i++) {
await this.addEquipment(id);
}
}
else if (t == "Relic") {
for (let i = 0; i < count; i++) {
await this.addRelic(id);
}
}
}
/**
* Adds the given amount of the virtual item with the given id to the player's inventory.
* @param id The item id.
* @param count The amount.
*/
public async addVirtualItem(id: number, count: number) {
// ToDo: Figure out which virtual item ID is what.
}
/**
* Adds the given amount of the material with the given id to the player's inventory.
* @param id The material id.
* @param count The amount.
*/
public async addMaterial(id: number, count: number) {
// Get info for the particular item we are trying to add.
const itemData = ItemExcel.fromId(id);
if (!itemData || itemData.ItemType != "Material") {
return;
}
const t = itemData.ItemType;
if (t != "Material") {
return;
}
// Get current item count for this ID and calculate new count.
const currentCount = this.db.materials[id] ?? 0;
const newCount = Math.min(currentCount + count, itemData.PileLimit);
// Update.
this.db.materials[id] = newCount;
await this.save();
// Send update.
this.sendMaterialUpdate();
}
/**
* Adds the given equipment to the player's inventory.
* @param equipment Either an `Equipment`, or the base id.
*/
public async addEquipment(equipment: number | Equipment) {
// If the parameter is a number, add a new equipment with this item ID as base.
if (typeof(equipment) == "number") {
const equip : Equipment = {
tid: equipment,
uniqueId: this.db.nextItemUid++,
level: 1,
rank: 1,
exp: 1,
isProtected: false,
promotion: 1,
baseAvatarId: 0
};
this.db.equipments.push(equip);
await this.save();
return;
}
// Otherwise, add the equipment object directly, but reset it's UID.
equipment.uniqueId = this.db.nextItemUid++;
this.db.equipments.push(equipment);
await this.save();
// Send update.
this.sendEquipmentUpdate();
}
/**
* Adds the given relic to the player's inventory.
* @param relic Either a `Relic`, or the base id.
*/
public async addRelic(relic: number | Relic) {
// Don't add relics for now until we figure out affix IDs, since the game kinda breaks with
// incorrect ones.
return;
// If the parameter is a number, add a new equipment with this item ID as base.
/*if (typeof(relic) == "number") {
const rel : Relic = {
tid: relic,
uniqueId: this.db.nextItemUid++,
level: 1,
exp: 1,
isProtected: false,
baseAvatarId: 0,
mainAffixId: 1,
subAffixList: []
};
this.db.relics.push(rel);
return;
}
// Otherwise, add the equipment object directly, but reset it's UID.
relic.uniqueId = this.db.nextItemUid++;
this.db.relics.push(relic);*/
}
/********************************************************************************
Remove items from the inventory directly.
********************************************************************************/
/**
* Removes the the given number of the given item (virtual or material) with the given ID.
* @param id The item ID.
* @param count The number to remove.
*/
public async removeItem(id: number, count: number) {
const itemData = ItemExcel.fromId(id);
if (!itemData) {
return ;
}
switch (itemData.ItemType) {
case "Virtual": await this.removeVirtualItem(id, count); break;
case "Material": await this.removeMaterial(id, count); break;
}
}
/**
* Removes the given amount of the given virtual item from the player's inventory.
* @param id The item id.
* @param count The amount.
*/
public async removeVirtualItem(id: number, count: number) {
await this.addVirtualItem(id, -count);
}
/**
* Removes the given amount of the given material from the player's inventory.
* @param id The item id.
* @param count The amount.
*/
public async removeMaterial(id: number, count: number) {
await this.addMaterial(id, -count);
}
/**
* Removes the given equipment player's inventory.
* @param equipment Either an `Equipment`, or the equipment's unique id.
*/
public async removeEquipment(equipment: number | Equipment) {
// Find index to delete.
const toDelete: number = (typeof(equipment) == "number") ? equipment : equipment.uniqueId;
const index = this.db.equipments.findIndex(i => i.uniqueId == toDelete);
// Delete and save.
this.db.equipments.splice(index, 1);
this.save();
// Send update.
this.sendEquipmentUpdate();
}
/**
* Removes the given relic player's inventory.
* @param relic Either a `Relic`, or the relic's unique id.
*/
public async removeRelic(relic: number | Relic) {
// Find index to delete.
const toDelete: number = (typeof(relic) == "number") ? relic : relic.uniqueId;
const index = this.db.relics.findIndex(i => i.uniqueId == toDelete);
// Delete and save.
this.db.relics.splice(index, 1);
this.save();
// Send update.
this.sendRelicUpdate()
}
/********************************************************************************
Pay items.
********************************************************************************/
/**
* Pay items (virtual items and materials).
* @param items The items to be paid.
* @returns True if paying succeeded, false otherwise.
*/
public async payItems(items: PayItemData[]) : Promise<boolean> {
// Check if the player has a sufficient amount of all necessary items.
for (const item of items) {
const currentCount = await this.getItemCount(item.id);
if (currentCount < item.count) {
return false;
}
}
// We have enough of everything - pay.
for (const item of items) {
await this.removeItem(item.id, item.count);
}
// Send update.
this.sendMaterialUpdate();
// Done.
return true;
}
/********************************************************************************
Player updating.
********************************************************************************/
private sendMaterialUpdate() {
this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
materialList: this.getMaterialList()
}));
}
private sendEquipmentUpdate() {
this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
equipmentList: this.getEquipmentList()
}));
}
private sendRelicUpdate() {
this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
relicList: this.getRelicsList()
}));
}
}

View File

@ -5,6 +5,7 @@ import Account from "./Account";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import Database from "./Database"; import Database from "./Database";
import { Scene } from "../game/Scene"; import { Scene } from "../game/Scene";
import Inventory from "./Inventory";
const c = new Logger("Player"); const c = new Logger("Player");
export interface LineupI { export interface LineupI {
@ -49,6 +50,7 @@ interface PlayerI {
export default class Player { export default class Player {
public readonly uid: number public readonly uid: number
public readonly scene: Scene; public readonly scene: Scene;
private inventory!: Inventory;
private constructor(readonly session: Session, public db: PlayerI) { private constructor(readonly session: Session, public db: PlayerI) {
this.uid = db._id; this.uid = db._id;
@ -105,6 +107,15 @@ export default class Player {
this.db.lineup.curIndex = curIndex; this.db.lineup.curIndex = curIndex;
} }
public async getInventory() : Promise<Inventory> {
// If this players inventory has not been loaded yet, do so now.
if (!this.inventory) {
this.inventory = await Inventory.loadOrCreate(this);
}
return this.inventory;
}
public static async create(session: Session, uid: number | string): Promise<Player | undefined> { public static async create(session: Session, uid: number | string): Promise<Player | undefined> {
if (typeof uid == "string") uid = Number(uid); if (typeof uid == "string") uid = Number(uid);
const acc = await Account.fromUID(uid); const acc = await Account.fromUID(uid);

View File

@ -80,10 +80,15 @@ export default class Session {
public async sync() { public async sync() {
const avatars = await Avatar.fromUID(this.player.db._id); const avatars = await Avatar.fromUID(this.player.db._id);
const inventory = await this.player.getInventory();
this.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ this.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
avatarSync: { avatarSync: {
avatarList: avatars.map(x => x.data), avatarList: avatars.map(x => x.data)
}, },
materialList: inventory.getMaterialList(),
equipmentList: inventory.getEquipmentList(),
relicList: inventory.getRelicsList(),
basicInfo: this.player.db.basicInfo basicInfo: this.player.db.basicInfo
})); }));

View File

@ -1,4 +1,5 @@
import { DoGachaCsReq, DoGachaScRsp, GachaItem, Item, ItemList } from "../../data/proto/StarRail"; import { DoGachaCsReq, DoGachaScRsp, GachaItem, Item, ItemList } from "../../data/proto/StarRail";
import { PayItemData } from "../../db/Inventory";
import Banners from "../../util/Banner"; import Banners from "../../util/Banner";
import Packet from "../kcp/Packet"; import Packet from "../kcp/Packet";
import Session from "../kcp/Session"; import Session from "../kcp/Session";
@ -8,6 +9,17 @@ export default async function handle(session: Session, packet: Packet) {
const body = packet.body as DoGachaCsReq; const body = packet.body as DoGachaCsReq;
const banner = Banners.config.find(banner => banner.gachaId === body.gachaId)!; const banner = Banners.config.find(banner => banner.gachaId === body.gachaId)!;
const combined = banner.rateUpItems4.concat(banner.rateUpItems5) const combined = banner.rateUpItems4.concat(banner.rateUpItems5)
// Pay currency.
const inventory = await session.player.getInventory();
const success = await inventory.payItems([{ id: banner.costItemId, count: body.gachaNum } as PayItemData]);
if (!success) {
session.send(DoGachaScRsp, {
retcode: 1
} as DoGachaScRsp);
}
//bad gachaing but whatever.... //bad gachaing but whatever....
//TODO: pity system, proper logic //TODO: pity system, proper logic
for(let i = 0; i < body.gachaNum; i++){ for(let i = 0; i < body.gachaNum; i++){

View File

@ -1,12 +1,14 @@
import { GetBagScRsp } from "../../data/proto/StarRail"; import { Equipment, GetBagScRsp, Material, Relic } from "../../data/proto/StarRail";
import Packet from "../kcp/Packet"; import Packet from "../kcp/Packet";
import Session from "../kcp/Session"; import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) { export default async function handle(session: Session, packet: Packet) {
const inventory = await session.player.getInventory();
session.send(GetBagScRsp, { session.send(GetBagScRsp, {
equipmentList: [], equipmentList: inventory.getEquipmentList(),
materialList: [], materialList: inventory.getMaterialList(),
relicList: [], relicList: inventory.getRelicsList(),
retcode: 0, retcode: 0,
rogueItemList: [], rogueItemList: [],
waitDelResourceList: [] waitDelResourceList: []

View File

@ -33,7 +33,7 @@ export default class Banners {
rateUpItems5: [ rateUpItems5: [
1102 1102
], ],
costItemId: -1 //unused for now costItemId: 101 // Star Rail Pass
} as Banner } as Banner
]; ];

View File

@ -0,0 +1,20 @@
import _ItemExcelTable from "../../data/excel/ItemExcelTable.json";
type ItemExcelTableEntry = typeof _ItemExcelTable[keyof typeof _ItemExcelTable]
const ItemExcelTable = _ItemExcelTable as { [key: string]: ItemExcelTableEntry };
export default class ItemExcel {
private constructor() {
}
public static all() : ItemExcelTableEntry[] {
return Object.values(ItemExcelTable);
}
public static fromId(id: number) : ItemExcelTableEntry {
return ItemExcelTable[id];
}
public static fromIds(ids: number[]): ItemExcelTableEntry[] {
return ids.map(id => ItemExcel.fromId(id));
}
}