mirror of
https://github.com/motajs/template.git
synced 2026-10-17 03:10:17 +08:00
feat: 商店系统实现
This commit is contained in:
parent
acf20f548d
commit
a15b79b29f
102
packages-user/data-base/src/shop/manager.ts
Normal file
102
packages-user/data-base/src/shop/manager.ts
Normal file
@ -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<string, IShopState> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
packages-user/data-base/src/shop/state.ts
Normal file
105
packages-user/data-base/src/shop/state.ts
Normal file
@ -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<number, number> = new Map();
|
||||
/** 此商店中,每个道具的买入价格 */
|
||||
private readonly buyPrice: Map<number, number> = new Map();
|
||||
/** 此商店中,每个道具的售出价格 */
|
||||
private readonly sellPrice: Map<number, number> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<number, number>;
|
||||
/** 商店道具的买入价与售出价 */
|
||||
readonly prices: ReadonlyMap<number, [buy: number, sell: number]>;
|
||||
/** 商店道具的买入价 */
|
||||
readonly buyPrice: ReadonlyMap<number, number>;
|
||||
/** 商店道具的售出价 */
|
||||
readonly sellPrice: ReadonlyMap<number, number>;
|
||||
}
|
||||
|
||||
export interface IShopState extends ISaveableContent<IShopStateSave> {
|
||||
export interface IShopState
|
||||
extends IDataCommonExtended, ISaveableContent<IShopStateSave> {
|
||||
/** 当前商店是否已经启用 */
|
||||
readonly enabled: boolean;
|
||||
/** 当前商店的标识符 */
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 向此商店添加或减少道具库存,会与原本的库存叠加
|
||||
@ -29,7 +34,7 @@ export interface IShopState extends ISaveableContent<IShopStateSave> {
|
||||
getCount(token: number | string): number;
|
||||
|
||||
/**
|
||||
* 迭代商店拥有的所有道具
|
||||
* 迭代商店拥有的所有道具,键表示道具的图块数字,值表示数量
|
||||
*/
|
||||
iterateItems(): Iterable<[number, number]>;
|
||||
|
||||
@ -42,7 +47,7 @@ export interface IShopState extends ISaveableContent<IShopStateSave> {
|
||||
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<IShopManagerSave> {
|
||||
export interface IShopManager
|
||||
extends IDataCommonExtended, ISaveableContent<IShopManagerSave> {
|
||||
/**
|
||||
* 添加新的商店
|
||||
* @param shop 商店状态对象
|
||||
|
||||
@ -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';
|
||||
|
||||
104
packages-user/data-system/src/shop/shop.ts
Normal file
104
packages-user/data-system/src/shop/shop.ts
Normal file
@ -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<IShopHeroAttr> | null = null;
|
||||
|
||||
private current: IShopState | null = null;
|
||||
|
||||
constructor(readonly state: IStateBase) {}
|
||||
|
||||
bindManager(manager: IShopManager | null): void {
|
||||
this.closeShop();
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
bindHero(hero: IHeroState<IShopHeroAttr> | 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;
|
||||
}
|
||||
}
|
||||
@ -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<IShopHeroAttr> | 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;
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user