194 lines
7.0 KiB
Dart
194 lines
7.0 KiB
Dart
|
|
import 'package:guru_app/analytics/guru_analytics.dart';
|
|||
|
|
import 'package:guru_app/database/guru_db.dart';
|
|||
|
|
import 'package:guru_app/financial/product/product_model.dart';
|
|||
|
|
import 'package:guru_app/guru_app.dart';
|
|||
|
|
import 'package:guru_app/inventory/db/inventory_database.dart';
|
|||
|
|
import 'package:guru_utils/database/batch/batch_aware.dart';
|
|||
|
|
import 'package:guru_utils/log/log.dart';
|
|||
|
|
import 'package:guru_utils/manifest/manifest.dart';
|
|||
|
|
|
|||
|
|
class InventoryCategory {
|
|||
|
|
static const String prop = "prop"; // 道具类
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class PropCategory {
|
|||
|
|
// 增益类:提供临时或永久的能力提升,如速度增加、力量提升、生命值恢复等。
|
|||
|
|
static const String boosts = 'Boosts';
|
|||
|
|
|
|||
|
|
// 减益类:施加在对手或玩家身上,造成能力降低、速度减慢、伤害增加等负面效果。
|
|||
|
|
static const String debuffs = 'Debuffs';
|
|||
|
|
|
|||
|
|
// 治疗类:恢复生命值或状态,解除负面效果。
|
|||
|
|
static const String healing = 'Healing';
|
|||
|
|
|
|||
|
|
// 防御类:提供防护,减少受到的伤害或完全避免某些类型的伤害。
|
|||
|
|
static const String defensive = 'Defensive';
|
|||
|
|
|
|||
|
|
// 攻击类:用于对敌人造成伤害或施加减益效果,如武器、陷阱、魔法等。
|
|||
|
|
static const String offensive = 'Offensive';
|
|||
|
|
|
|||
|
|
// 辅助类:提供团队增益、增强队友能力、提供战术优势等非直接攻击手段的道具。
|
|||
|
|
static const String supportive = 'Supportive';
|
|||
|
|
|
|||
|
|
// 探索类:用于揭示地图、发现隐藏物品、解锁新区域或提供关键信息的道具。
|
|||
|
|
static const String exploratory = 'Exploratory';
|
|||
|
|
|
|||
|
|
// 交互类:与游戏世界中的其他元素或玩家进行交互的道具,如钥匙、开关、通信设备等。
|
|||
|
|
static const String interactive = 'Interactive';
|
|||
|
|
|
|||
|
|
// 资源类:用于制造、升级或交易的材料和货币类型道具。
|
|||
|
|
static const String resources = 'Resources';
|
|||
|
|
|
|||
|
|
// 限定类:只能使用一定次数的道具,使用后消失或需要充能。
|
|||
|
|
static const String limitedUse = 'LimitedUse';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mixin InventoryDelegate {
|
|||
|
|
// 返回指定id的库存类别
|
|||
|
|
// 如 hint,zoom这些道具统一返回
|
|||
|
|
String getInventoryCategory(String id) {
|
|||
|
|
return InventoryCategory.prop;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<List<StockItem>> getMigrateStockItems() async {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class StockItem {
|
|||
|
|
final String sku;
|
|||
|
|
final int amount;
|
|||
|
|
final int attr;
|
|||
|
|
final DateTime? expired;
|
|||
|
|
|
|||
|
|
const StockItem.consumable(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.consumable;
|
|||
|
|
|
|||
|
|
const StockItem.permanent(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.permanent;
|
|||
|
|
|
|||
|
|
const StockItem.igc(this.amount, {this.sku = "igc"})
|
|||
|
|
: attr = DetailsAttr.consumable,
|
|||
|
|
expired = null;
|
|||
|
|
|
|||
|
|
StockItem.fromDetails(Details details)
|
|||
|
|
: sku = details.sku,
|
|||
|
|
attr = details.attr,
|
|||
|
|
amount = details.amount,
|
|||
|
|
expired = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class InventoryManager with BatchAware<InventoryItem> {
|
|||
|
|
static final InventoryManager instance = InventoryManager._();
|
|||
|
|
|
|||
|
|
InventoryManager._();
|
|||
|
|
|
|||
|
|
String getInventoryCategory(String id) {
|
|||
|
|
return GuruApp.instance.protocol.inventoryDelegate?.getInventoryCategory(id) ??
|
|||
|
|
InventoryCategory.prop;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future init() async {
|
|||
|
|
final batch = await GuruDB.instance.loadInventoryItems();
|
|||
|
|
processBatchData(batch);
|
|||
|
|
await _migrate();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future _migrate() async {
|
|||
|
|
final migrateStockItems =
|
|||
|
|
await GuruApp.instance.protocol.inventoryDelegate?.getMigrateStockItems() ?? [];
|
|||
|
|
|
|||
|
|
if (migrateStockItems.isNotEmpty) {
|
|||
|
|
final needMigrateItems = migrateStockItems.where((item) => !exists(item.sku)).toList();
|
|||
|
|
await acquire(needMigrateItems, TransactionMethod.migrate, "migrate");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
///
|
|||
|
|
/// 通过[method]中的的特定[specific]方式 获得了指定的 [items]
|
|||
|
|
/// * method: iap -> specific: sku
|
|||
|
|
/// * method: igc -> specific: coin/gems...
|
|||
|
|
/// * method: reward -> specific: ads/lottery/daily/...
|
|||
|
|
/// * method: bonus -> specific: ads/other/...
|
|||
|
|
/// * method: igb -> specific: hint/hammer/swap/magic/..
|
|||
|
|
///
|
|||
|
|
/// method 最终会在 earnVirtualCurrency 中成为 item_category
|
|||
|
|
/// specific 最终会在 earnVirtualCurrency 中成为 item_name
|
|||
|
|
///
|
|||
|
|
Future acquire(List<StockItem> items, TransactionMethod method, String specific,
|
|||
|
|
{String? scene}) async {
|
|||
|
|
final acquired = <InventoryItem>[];
|
|||
|
|
for (var item in items) {
|
|||
|
|
final category = getInventoryCategory(item.sku);
|
|||
|
|
final invItem = getData(item.sku)
|
|||
|
|
?.acquire(method, item.amount, expiredAt: item.expired?.millisecondsSinceEpoch) ??
|
|||
|
|
InventoryItem.create(item.sku, category, item.attr,
|
|||
|
|
balance: item.amount,
|
|||
|
|
method: method,
|
|||
|
|
expireAt: item.expired?.millisecondsSinceEpoch ?? -1);
|
|||
|
|
acquired.add(invItem);
|
|||
|
|
|
|||
|
|
GuruAnalytics.instance.logEarnVirtualCurrency(
|
|||
|
|
virtualCurrencyName: item.sku,
|
|||
|
|
method: convertTransactionMethodName(method),
|
|||
|
|
specific: specific,
|
|||
|
|
balance: invItem.balance,
|
|||
|
|
value: item.amount,
|
|||
|
|
scene: scene);
|
|||
|
|
}
|
|||
|
|
final batchData = await GuruDB.instance.updateInventoryItems(acquired);
|
|||
|
|
processBatchData(batchData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ConsumeResult? _consumeItem(StockItem item, Manifest redeemed, {bool timeSensitiveOnly = false}) {
|
|||
|
|
final inventoryItem = getData(item.sku);
|
|||
|
|
return timeSensitiveOnly
|
|||
|
|
? inventoryItem?.consumeTimeSensitiveOnly(item.amount,
|
|||
|
|
scene: redeemed.scene, transactionTs: redeemed.transactionTs)
|
|||
|
|
: inventoryItem?.consume(item.amount, scene: redeemed.scene);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 消耗指定的道具[items],如果可以成功消费,便可得到相应的[redeemed]清单
|
|||
|
|
Future<bool> consume(List<StockItem> items, Manifest redeemed,
|
|||
|
|
{bool timeSensitiveOnly = false}) async {
|
|||
|
|
final consumed = <InventoryItem>[];
|
|||
|
|
bool isConsumed = true;
|
|||
|
|
for (var item in items) {
|
|||
|
|
/// 这里只是针对对应的 SKU 商品进行 consume标记,真正的消耗在下面的updateInventoryItems中
|
|||
|
|
final result = _consumeItem(item, redeemed, timeSensitiveOnly: timeSensitiveOnly);
|
|||
|
|
if (result == null) {
|
|||
|
|
Log.e("consume failed! Not Found inventory item: ${item.sku}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
consumed.add(result.item);
|
|||
|
|
// 这里如果没有消耗掉,将跳出
|
|||
|
|
if (!result.consumed) {
|
|||
|
|
Log.w("consume failed: ${item.sku} ${item.amount}");
|
|||
|
|
isConsumed = false;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (consumed.isEmpty) {
|
|||
|
|
return false;
|
|||
|
|
} else {
|
|||
|
|
if (isConsumed && consumed.length == items.length) {
|
|||
|
|
for (int i = 0; i < items.length; ++i) {
|
|||
|
|
GuruAnalytics.instance.logSpendCredits(
|
|||
|
|
redeemed.contentId, redeemed.category, items[i].amount,
|
|||
|
|
virtualCurrencyName: consumed[i].sku,
|
|||
|
|
balance: consumed[i].balance,
|
|||
|
|
scene: redeemed.scene);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 这里会更新消耗掉的道具
|
|||
|
|
final batchData = await GuruDB.instance.updateInventoryItems(consumed);
|
|||
|
|
processBatchData(batchData);
|
|||
|
|
return isConsumed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool canAfford(String id, int amount) {
|
|||
|
|
final item = getData(id);
|
|||
|
|
return item != null && item.balance >= amount;
|
|||
|
|
}
|
|||
|
|
}
|