diff --git a/packages-user/data-base/src/shop/manager.ts b/packages-user/data-base/src/shop/manager.ts new file mode 100644 index 0000000..3aed4c3 --- /dev/null +++ b/packages-user/data-base/src/shop/manager.ts @@ -0,0 +1,102 @@ +import { IDataCommon, SaveCompression } from '@user/data-common'; +import { IShopManager, IShopManagerSave, IShopState } from './types'; +import { logger } from '@motajs/common'; +import { ShopState } from './state'; +import { isNil } from 'lodash-es'; + +export class ShopManager implements IShopManager { + /** 当前管理器包含的商店列表 */ + private readonly shops: Map = new Map(); + + constructor(readonly state: IDataCommon) {} + + /** + * 传入商店 id 或实例,输出商店实例 + * @param shop 商店的 id 或其实例 + */ + private getShopState(shop: string | IShopState): IShopState | null { + if (typeof shop === 'string') { + return this.shops.get(shop) ?? null; + } else { + return shop; + } + } + + /** + * 传入商店 id 或实例,输出商店 id + * @param shop 商店的 id 或其实例 + */ + private getShopId(shop: string | IShopState): string | undefined { + if (typeof shop === 'string') { + return shop; + } else { + return shop.id; + } + } + + addShop(shop: IShopState): void { + if (this.shops.has(shop.id)) { + logger.warn(198, shop.id); + return; + } + this.shops.set(shop.id, shop); + } + + removeShop(shop: string | IShopState): void { + const id = this.getShopId(shop); + if (!isNil(id)) this.shops.delete(id); + } + + getShop(id: string): IShopState | null { + return this.shops.get(id) ?? null; + } + + hasShop(shop: string | IShopState): boolean { + const id = this.getShopId(shop); + return isNil(id) || this.shops.has(id); + } + + iterateShops(): Iterable<[string, IShopState]> { + return this.shops; + } + + *iterateEnabledShops(): Iterable<[string, IShopState]> { + for (const [key, state] of this.shops) { + if (state.enabled) yield [key, state]; + } + } + + *iterateDisabledShops(): Iterable<[string, IShopState]> { + for (const [key, state] of this.shops) { + if (!state.enabled) yield [key, state]; + } + } + + enableShop(shop: string | IShopState): void { + const state = this.getShopState(shop); + if (state) state.enable(); + } + + disableShop(shop: string | IShopState): void { + const state = this.getShopState(shop); + if (state) state.disable(); + } + + saveState(compression: SaveCompression): IShopManagerSave { + const list = [...this.shops.values()].map(v => + v.saveState(compression) + ); + return { + list + }; + } + + loadState(state: IShopManagerSave, compression: SaveCompression): void { + this.shops.clear(); + for (const save of state.list) { + const shop: IShopState = new ShopState(save.id, this.state); + shop.loadState(save, compression); + this.shops.set(save.id, shop); + } + } +} diff --git a/packages-user/data-base/src/shop/state.ts b/packages-user/data-base/src/shop/state.ts new file mode 100644 index 0000000..8d51f32 --- /dev/null +++ b/packages-user/data-base/src/shop/state.ts @@ -0,0 +1,105 @@ +import { IDataCommon } from '@user/data-common'; +import { IShopState, IShopStateSave } from './types'; +import { isNil } from 'lodash-es'; +import { logger } from '@motajs/common'; + +export class ShopState implements IShopState { + enabled: boolean = false; + + /** 此商店拥有的所有道具及其库存 */ + private readonly items: Map = new Map(); + /** 此商店中,每个道具的买入价格 */ + private readonly buyPrice: Map = new Map(); + /** 此商店中,每个道具的售出价格 */ + private readonly sellPrice: Map = new Map(); + + constructor( + readonly id: string, + readonly state: IDataCommon + ) {} + + addItem(token: number | string, count: number): void { + const num = this.state.tileStore.num(token); + if (isNil(num)) { + logger.warn(196, token.toString()); + return; + } + // 注意,这里不把库存为 0 的置空是因为置空之后迭代不到,商店将不再显示这个道具 + // 但是商店理应显示库存为 0 的道具,一是因为玩家可能需要卖出,二是因为玩家可能需要 0 库存这一信息 + const before = this.items.getOrInsert(num, 0); + this.items.set(num, before + count); + } + + getCount(token: number | string): number { + const num = this.state.tileStore.num(token); + if (isNil(num)) { + logger.warn(196, token.toString()); + return -1; + } + return this.items.get(num) ?? -1; + } + + iterateItems(): Iterable<[number, number]> { + return this.items; + } + + setPrice(item: number | string, buy: number, sell: number): void { + const num = this.state.tileStore.num(item); + if (isNil(num)) { + logger.warn(196, item.toString()); + return; + } + this.buyPrice.set(num, buy); + this.sellPrice.set(num, sell); + } + + getPrice(item: number | string): [buy: number, sell: number] { + const num = this.state.tileStore.num(item); + if (isNil(num)) { + logger.warn(196, item.toString()); + return [-1, -1]; + } + const buy = this.buyPrice.get(num); + const sell = this.sellPrice.get(num); + if (isNil(buy) || isNil(sell)) return [-1, -1]; + else return [buy, sell]; + } + + enable(): void { + this.enabled = true; + } + + disable(): void { + this.enabled = false; + } + + saveState(): IShopStateSave { + return { + id: this.id, + enabled: this.enabled, + items: this.items, + buyPrice: this.buyPrice, + sellPrice: this.sellPrice + }; + } + + loadState(state: IShopStateSave): void { + if (state.id !== this.id) { + logger.warn(197, this.id, state.id); + return; + } + this.enabled = state.enabled; + this.items.clear(); + for (const [key, value] of state.items) { + this.items.set(key, value); + } + this.buyPrice.clear(); + for (const [key, value] of state.buyPrice) { + this.buyPrice.set(key, value); + } + this.sellPrice.clear(); + for (const [key, value] of state.sellPrice) { + this.sellPrice.set(key, value); + } + } +} diff --git a/packages-user/data-base/src/shop/types.ts b/packages-user/data-base/src/shop/types.ts index b4061da..8e84b52 100644 --- a/packages-user/data-base/src/shop/types.ts +++ b/packages-user/data-base/src/shop/types.ts @@ -1,4 +1,4 @@ -import { ISaveableContent } from '@user/data-common'; +import { IDataCommonExtended, ISaveableContent } from '@user/data-common'; export interface IShopStateSave { /** 商店 id */ @@ -7,13 +7,18 @@ export interface IShopStateSave { readonly enabled: boolean; /** 商店拥有的库存 */ readonly items: ReadonlyMap; - /** 商店道具的买入价与售出价 */ - readonly prices: ReadonlyMap; + /** 商店道具的买入价 */ + readonly buyPrice: ReadonlyMap; + /** 商店道具的售出价 */ + readonly sellPrice: ReadonlyMap; } -export interface IShopState extends ISaveableContent { +export interface IShopState + extends IDataCommonExtended, ISaveableContent { /** 当前商店是否已经启用 */ readonly enabled: boolean; + /** 当前商店的标识符 */ + readonly id: string; /** * 向此商店添加或减少道具库存,会与原本的库存叠加 @@ -29,7 +34,7 @@ export interface IShopState extends ISaveableContent { getCount(token: number | string): number; /** - * 迭代商店拥有的所有道具 + * 迭代商店拥有的所有道具,键表示道具的图块数字,值表示数量 */ iterateItems(): Iterable<[number, number]>; @@ -42,7 +47,7 @@ export interface IShopState extends ISaveableContent { setPrice(item: number | string, buy: number, sell: number): void; /** - * 获取道具价格 + * 获取道具价格,如果道具不存在则返回 -1 * @param item 道具数字或 id */ getPrice(item: number | string): [buy: number, sell: number]; @@ -63,7 +68,8 @@ export interface IShopManagerSave { readonly list: IShopStateSave[]; } -export interface IShopManager extends ISaveableContent { +export interface IShopManager + extends IDataCommonExtended, ISaveableContent { /** * 添加新的商店 * @param shop 商店状态对象 diff --git a/packages-user/data-state/src/enemy/index.ts b/packages-user/data-state/src/enemy/index.ts index 23e0ffc..0bd397f 100644 --- a/packages-user/data-state/src/enemy/index.ts +++ b/packages-user/data-state/src/enemy/index.ts @@ -2,7 +2,6 @@ export * from './aura'; export * from './calculator'; export * from './comparer'; export * from './final'; -export * from './legacy'; export * from './mapDamage'; export * from './special'; export * from './types'; diff --git a/packages-user/data-system/src/shop/shop.ts b/packages-user/data-system/src/shop/shop.ts new file mode 100644 index 0000000..8050db2 --- /dev/null +++ b/packages-user/data-system/src/shop/shop.ts @@ -0,0 +1,104 @@ +import { + IShopManager, + IHeroState, + IShopState, + IStateBase +} from '@user/data-base'; +import { IShopHeroAttr, IShopSystem, ShopTradeResult } from './types'; +import { logger } from '@motajs/common'; +import { isNil } from 'lodash-es'; +import { shouldReplay } from '@user/data-common'; + +export class ShopSystem implements IShopSystem { + manager: IShopManager | null = null; + hero: IHeroState | null = null; + + private current: IShopState | null = null; + + constructor(readonly state: IStateBase) {} + + bindManager(manager: IShopManager | null): void { + this.closeShop(); + this.manager = manager; + } + + bindHero(hero: IHeroState | null): void { + this.closeShop(); + this.hero = hero; + } + + @shouldReplay('Openning shop should be replayed.') + openShop(shop: IShopState): void { + this.current = shop; + } + + getCurrentShop(): IShopState | null { + return this.current; + } + + @shouldReplay('Closing shop should be replayed.') + closeShop(): void { + this.current = null; + } + + @shouldReplay('Buying items should be replayed.') + buyItem(item: number | string, count: number): ShopTradeResult { + if (!this.current || !this.hero) { + logger.warn(199); + return ShopTradeResult.Invalid; + } + const num = this.state.tileStore.num(item); + if (isNil(num)) { + logger.warn(200, item.toString()); + return ShopTradeResult.Invalid; + } + + const shop = this.current; + const hero = this.hero.getModifiableAttribute(); + + const currCount = shop.getCount(count); + if (currCount < count) return ShopTradeResult.ShopLack; + + const [buyPrice] = shop.getPrice(num); + if (buyPrice < 0) { + return ShopTradeResult.Invalid; + } + const need = buyPrice * count; + const owned = hero.getBaseAttribute('money'); + if (owned < need) { + return ShopTradeResult.MoneyLack; + } + + hero.add('money', -need); + shop.addItem(num, -count); + return ShopTradeResult.Success; + } + + sellItem(item: number | string, count: number): ShopTradeResult { + if (!this.current || !this.hero) { + logger.warn(199); + return ShopTradeResult.Invalid; + } + const num = this.state.tileStore.num(item); + if (isNil(num)) { + logger.warn(200, item.toString()); + return ShopTradeResult.Invalid; + } + + const shop = this.current; + const hero = this.hero.getModifiableAttribute(); + const pack = this.hero.items; + + const currCount = pack.itemCount(num); + if (currCount < count) return ShopTradeResult.HeroLack; + + const [, sellPrice] = shop.getPrice(num); + if (sellPrice < 0) { + return ShopTradeResult.Invalid; + } + const need = sellPrice * count; + hero.add('money', need); + shop.addItem(num, count); + return ShopTradeResult.Success; + } +} diff --git a/packages-user/data-system/src/shop/types.ts b/packages-user/data-system/src/shop/types.ts index 1c13eda..6e251c7 100644 --- a/packages-user/data-system/src/shop/types.ts +++ b/packages-user/data-system/src/shop/types.ts @@ -1,16 +1,39 @@ -import { IHeroState, IShopManager, IShopState } from '@user/data-base'; +import { + IDataBaseExtended, + IHeroState, + IShopManager, + IShopState +} from '@user/data-base'; export interface IShopHeroAttr { /** 勇士拥有的金币 */ money: number; } -export interface IShopSystem { +export const enum ShopTradeResult { + /** 成功购入或售出 */ + Success, + /** 操作不合法,一般是因为道具不存在或未打开商店 */ + Invalid, + /** 商店库存不足,无法购入这么多道具 */ + ShopLack, + /** 玩家库存不足,无法卖出这么多道具 */ + HeroLack, + /** 玩家货币不足,无法购入这么多道具 */ + MoneyLack +} + +export interface IShopSystem extends IDataBaseExtended { + /** 当前使用的商店管理器 */ + readonly manager: IShopManager | null; + /** 当前绑定的勇士状态对象 */ + readonly hero: IHeroState | null; + /** * 绑定商店管理器 * @param manager 商店管理器 */ - bindManager(manager: IShopManager): void; + bindManager(manager: IShopManager | null): void; /** * 绑定系统使用的勇士状态对象 @@ -24,6 +47,11 @@ export interface IShopSystem { */ openShop(shop: IShopState): void; + /** + * 获取当前已打开的商店状态对象 + */ + getCurrentShop(): IShopState | null; + /** * 关闭当前打开的商店 */ @@ -34,12 +62,12 @@ export interface IShopSystem { * @param item 道具数字或 id * @param count 购买的道具数量 */ - buyItem(item: number | string, count: number): void; + buyItem(item: number | string, count: number): ShopTradeResult; /** * 在当前打开的商店里面售出指定道具 * @param item 道具数字或 id * @param count 卖出的道具数量 */ - sellItem(item: number | string, count: number): void; + sellItem(item: number | string, count: number): ShopTradeResult; } diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 37865ba..dc136b3 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -279,6 +279,11 @@ "193": "Cannot load item state since no item id registration for item num $1.", "194": "Expected passive replay command executed during the last active replay command executing, but a passive command $1 was got after active replay command $2 executing. Replay will continue and ignore this issue.", "195": "Cannot compare equipments since both equipments not exists. Equip A: $1, Equip B: $2.", + "196": "Cannot change item $1 info in shop since the item does not exist.", + "197": "Cannot load shop state since id mismatch: '$1' vs '$2'.", + "198": "Duplicate shop id '$1', old shop will be overwritten.", + "199": "A shop state instance and a hero state instance binding is needed before buy or sell items.", + "200": "Cannot buy or sell item since target item '$1' does not exist.", "1001": "Event(setBlock): Unknown tile '$1' when setting block." } }