|
|
8 mesi fa | |
|---|---|---|
| .. | ||
| AdminControllers | 8 mesi fa | |
| Commands | 8 mesi fa | |
| Config | 8 mesi fa | |
| Enums | 8 mesi fa | |
| Events | 8 mesi fa | |
| Exceptions | 8 mesi fa | |
| Jobs | 8 mesi fa | |
| Listeners | 8 mesi fa | |
| Models | 8 mesi fa | |
| Providers | 8 mesi fa | |
| Queues | 8 mesi fa | |
| Repositorys | 8 mesi fa | |
| Services | 8 mesi fa | |
| Tests | 8 mesi fa | |
| Validations | 8 mesi fa | |
| Validators | 8 mesi fa | |
| GUILD.md | 8 mesi fa | |
| README.md | 8 mesi fa | |
| WORK.md | 8 mesi fa | |
游戏物品模块
GameItems模块主要负责游戏内所有物品的管理,包括但不限于:
核心功能特点:
该模块为游戏内经济系统和玩家进度提供基础支持,是连接游戏多个子系统的核心模块。
游戏物品模块采用关系型数据库设计,通过多个相互关联的数据表实现物品管理、用户物品关联、属性存储和宝箱配置等功能。数据结构设计遵循以下原则:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 物品ID,主键 |
| name | varchar | 物品名称 |
| description | text | 物品描述 |
| type | tinyint | 物品类型(1:消耗品, 2:装备, 3:材料, 4:任务物品, 5:宝箱...) |
| rarity | tinyint | 稀有度(1:普通, 2:稀有, 3:史诗, 4:传说...) |
| icon | varchar | 物品图标路径 |
| max_stack | int | 最大堆叠数量 |
| sell_price | int | 出售价格 |
| display_attributes | json | 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性 |
| numeric_attributes | json | 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性 |
| min_drop_count | int | 宝箱最小掉落物品数量(仅宝箱类型物品有效) |
| max_drop_count | int | 宝箱最大掉落物品数量(仅宝箱类型物品有效) |
| global_expire_at | timestamp | 物品全局过期时间(可为空) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 唯一物品ID,主键 |
| item_id | int | 关联的基础物品ID,外键关联items表 |
| name | varchar | 物品名称(可以与基础物品不同,如“锐利的钢刀”) |
| display_attributes | json | 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性 |
| numeric_attributes | json | 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性 |
| expire_at | timestamp | 物品过期时间(可为空) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID,主键 |
| user_id | int | 用户ID,外键 |
| item_id | int | 统一属性物品ID,外键关联items表(当该字段有值时,unique_item_id应为空) |
| unique_item_id | int | 单独属性物品ID,外键关联unique_items表(当该字段有值时,item_id应为空) |
| quantity | int | 数量(对于单独属性物品,该值始终为1) |
| expire_at | timestamp | 用户物品过期时间(可为空) |
| created_at | timestamp | 获取时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID,主键 |
| chest_id | int | 宝箱物品ID,外键关联items表 |
| item_id | int | 可能获得的物品ID |
| is_unique | tinyint | 是否生成单独属性物品(0:否, 1:是) |
| min_quantity | int | 最小数量 |
| max_quantity | int | 最大数量 |
| weight | int | 权重,决定获取概率 |
| allow_duplicate | tinyint | 是否允许在同一宝箱中重复掉落(0:不允许, 1:允许) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
为了确保系统的高性能运行,我们对关键字段进行了索引设计:
| 表名 | 索引字段 | 索引类型 | 说明 |
|---|---|---|---|
| items | id | 主键 | 物品ID主键索引 |
| items | type | 普通索引 | 加速按物品类型查询 |
| items | global_expire_at | 普通索引 | 加速过期物品查询 |
| unique_items | id | 主键 | 唯一物品ID主键索引 |
| unique_items | item_id | 普通索引 | 加速根据基础物品查询唯一物品 |
| unique_items | expire_at | 普通索引 | 加速过期物品查询 |
| user_items | user_id, item_id | 复合索引 | 加速用户统一属性物品查询 |
| user_items | user_id, unique_item_id | 复合索引 | 加速用户单独属性物品查询 |
| user_items | expire_at | 普通索引 | 加速过期物品查询 |
| chest_items | chest_id | 普通索引 | 加速宝箱内容查询 |
| chest_items | item_id | 普通索引 | 加速物品在宝箱中的查询 |
| chest_items | is_unique | 普通索引 | 加速查询生成唯一物品的宝箱配置 |
以下是数据模型之间的关联关系图:
graph TD
User["User"] --- UserItems{"user_items<br>M:N"}
UserItems --- Item["Item<br>统一属性物品"]
UserItems --- UniqueItem["unique_items<br>单独属性物品"]
UniqueItem -->|"N:1"| Item
subgraph "宝箱系统"
ChestItem["Item<br>(type=5)宝箱"] -->|"1:N"| ChestConfig["chest_items<br>宝箱配置"]
ChestConfig -->|"N:1"| ItemDrop["Item<br>掉落物品"]
ChestConfig -->|"is_unique=1"| UniqueItem
end
classDef entity fill:#f9f,stroke:#333,stroke-width:2px;
classDef junction fill:#bbf,stroke:#33f,stroke-width:2px;
class User,Item,UniqueItem,ChestItem,ItemDrop entity;
class UserItems,ChestConfig junction;
图表说明:
为了支持“每个用户拥有的同类物品可以有不同属性”的需求(如不同的刀有不同的属性),我们采用了两种物品类型的设计:
该设计允许我们实现以下功能:
物品类型分离
items表存储统一属性物品,属性以JSON格式存储unique_items表存储单独属性物品,属性以JSON格式存储用户物品关联的灵活性
user_items表中的item_id和unique_item_id字段互斥,一个记录只能关联其中一种物品quantity始终为1宝箱生成机制
chest_items表中的is_unique字段控制生成类型// 为用户创建一把具有单独属性的刀
public function createUniqueWeapon($userId, $baseItemId)
{
// 开始事务
DB::beginTransaction();
try {
// 1. 获取基础物品信息
$baseItem = Item::findOrFail($baseItemId);
// 2. 生成随机属性
// 继承基础物品的展示属性
$baseDisplayAttributes = json_decode($baseItem->display_attributes, true) ?? [];
$displayAttributes = $baseDisplayAttributes;
// 添加自定义展示属性
$customDisplayAttributes = [
'appearance' => '闪亮的刀身上刻有精美的纹理',
'material' => '特殊合金打造',
'description' => '这把刀经过精心锻造,锐利无比'
];
// 合并展示属性,自定义属性会覆盖基础属性
$displayAttributes = array_merge($displayAttributes, $customDisplayAttributes);
// 继承基础物品的数值属性
$baseNumericAttributes = json_decode($baseItem->numeric_attributes, true) ?? [];
$numericAttributes = $baseNumericAttributes;
// 生成随机数值属性
$randomNumericAttributes = [
'attack' => mt_rand(10, 20),
'durability' => mt_rand(80, 100),
'sharpness' => mt_rand(1, 5),
'critical_chance' => mt_rand(5, 15) / 100 // 暴击几率
];
// 合并数值属性,随机属性会覆盖基础属性
$numericAttributes = array_merge($numericAttributes, $randomNumericAttributes);
// 3. 创建单独属性物品
$uniqueItem = new UniqueItem();
$uniqueItem->item_id = $baseItemId;
$uniqueItem->name = "锐利的" . $baseItem->name; // 可以自定义名称
$uniqueItem->display_attributes = json_encode($displayAttributes);
$uniqueItem->numeric_attributes = json_encode($numericAttributes);
$uniqueItem->save();
// 4. 关联到用户
$userItem = new UserItem();
$userItem->user_id = $userId;
$userItem->unique_item_id = $uniqueItem->id; // 注意这里使用unique_item_id而非item_id
$userItem->quantity = 1; // 单独属性物品数量始终为1
$userItem->save();
DB::commit();
return [
'user_item' => $userItem,
'unique_item' => $uniqueItem
];
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
// 获取用户的所有单独属性武器
public function getUserUniqueWeapons($userId, $baseItemId = null)
{
// 构建查询
$query = UserItem::where('user_id', $userId)
->whereNotNull('unique_item_id'); // 只查询单独属性物品
// 如果指定了基础物品ID,则只查询该类型的武器
if ($baseItemId) {
$query->whereHas('uniqueItem', function($q) use ($baseItemId) {
$q->where('item_id', $baseItemId);
});
}
// 获取用户物品记录,并预加载单独属性物品信息
$userItems = $query->with('uniqueItem.baseItem')->get();
$result = [];
foreach ($userItems as $userItem) {
$uniqueItem = $userItem->uniqueItem;
$baseItem = $uniqueItem->baseItem;
$weaponData = [
'id' => $userItem->id,
'unique_item_id' => $uniqueItem->id,
'base_item_id' => $baseItem->id,
'name' => $uniqueItem->name,
'base_name' => $baseItem->name,
'display_attributes' => json_decode($uniqueItem->display_attributes, true),
'numeric_attributes' => json_decode($uniqueItem->numeric_attributes, true),
'obtained_at' => $userItem->created_at->format('Y-m-d H:i:s')
];
$result[] = $weaponData;
}
return $result;
}
// 为用户添加统一属性物品
public function addItemToUser($userId, $itemId, $quantity = 1)
{
// 查找用户是否已有该物品
$userItem = UserItem::where('user_id', $userId)
->where('item_id', $itemId)
->whereNull('unique_item_id') // 确保是统一属性物品
->first();
if ($userItem) {
// 如果已有,增加数量
$userItem->quantity += $quantity;
$userItem->save();
} else {
// 如果没有,创建新记录
$userItem = new UserItem();
$userItem->user_id = $userId;
$userItem->item_id = $itemId;
$userItem->quantity = $quantity;
$userItem->save();
}
return $userItem;
}
在涉及多表操作的关键业务逻辑中,使用数据库事务确保数据一致性:
// 控制器中
public function showUserItems(Request $request)
{
$user = Auth::user();
$items = app(GameItemService::class)->getUserItems($user->id);
return view('inventory', ['items' => $items]);
}
// 控制器中
public function useItem(Request $request)
{
$user = Auth::user();
$itemId = $request->input('item_id');
$quantity = $request->input('quantity', 1);
$result = app(GameItemService::class)->useItem($user->id, $itemId, $quantity);
if ($result['success']) {
return response()->json([
'code' => 0,
'message' => '物品使用成功',
'data' => $result['data']
]);
}
return response()->json([
'code' => 1,
'message' => $result['message']
]);
}
// 在任务完成奖励中
public function completeQuest($userId, $questId)
{
// 处理任务完成逻辑...
// 发放物品奖励
$rewards = [
['item_id' => 1, 'quantity' => 5], // 5个生命药水
['item_id' => 10, 'quantity' => 1, 'expire_days' => 7], // 1把稀有武器,7天后过期
];
foreach ($rewards as $reward) {
$expireAt = null;
if (isset($reward['expire_days'])) {
$expireAt = now()->addDays($reward['expire_days']);
}
app(GameItemService::class)->addItemToUser(
$userId,
$reward['item_id'],
$reward['quantity'],
$expireAt
);
}
// 返回奖励信息...
}
// 在使用物品前检查
public function checkItemValidity($userId, $itemId)
{
$userItem = UserItem::where('user_id', $userId)
->where('item_id', $itemId)
->first();
if (!$userItem) {
return ['valid' => false, 'message' => '物品不存在'];
}
// 检查用户物品过期时间
if ($userItem->expire_at && $userItem->expire_at < now()) {
return ['valid' => false, 'message' => '物品已过期'];
}
// 检查全局过期时间
$item = Item::find($itemId);
if ($item->global_expire_at && $item->global_expire_at < now()) {
return ['valid' => false, 'message' => '物品已全局过期'];
}
return ['valid' => true];
}
// 宝箱服务中
public function openChest($userId, $chestId, $quantity = 1)
{
// 验证用户是否拥有足够的宝箱
$userChest = UserItem::where('user_id', $userId)
->where('item_id', $chestId)
->first();
if (!$userChest || $userChest->quantity < $quantity) {
return [
'success' => false,
'message' => '宝箱数量不足'
];
}
// 检查宝箱是否过期
$validityCheck = $this->checkItemValidity($userId, $chestId);
if (!$validityCheck['valid']) {
return [
'success' => false,
'message' => $validityCheck['message']
];
}
// 获取宝箱配置
$chestConfig = ChestItem::where('chest_id', $chestId)->get();
if ($chestConfig->isEmpty()) {
return [
'success' => false,
'message' => '宝箱配置错误'
];
}
// 开始事务
DB::beginTransaction();
try {
// 扣除宝箱
$userChest->quantity -= $quantity;
if ($userChest->quantity <= 0) {
$userChest->delete();
} else {
$userChest->save();
}
$rewards = [];
// 处理每个宝箱
for ($i = 0; $i < $quantity; $i++) {
// 获取当前宝箱的掉落物品数量范围
$chest = Item::find($chestId);
$dropItemsCount = mt_rand(
$chest->getAttribute('min_drop_count', 1),
$chest->getAttribute('max_drop_count', 3)
);
// 为每个宝箱随机选择多个物品
$selectedItems = [];
$availableConfigs = $chestConfig->toArray();
// 选择多个物品
for ($j = 0; $j < $dropItemsCount; $j++) {
if (empty($availableConfigs)) {
break;
}
// 随机选择物品
$totalWeight = array_sum(array_column($availableConfigs, 'weight'));
$randomWeight = mt_rand(1, $totalWeight);
$currentWeight = 0;
$selectedIndex = 0;
foreach ($availableConfigs as $index => $config) {
$currentWeight += $config['weight'];
if ($randomWeight <= $currentWeight) {
$selectedIndex = $index;
break;
}
}
// 获取选中的配置
$selectedConfig = $availableConfigs[$selectedIndex];
// 随机决定数量
$itemQuantity = mt_rand($selectedConfig['min_quantity'], $selectedConfig['max_quantity']);
// 获取物品信息
$item = Item::find($selectedConfig['item_id']);
// 添加到用户背包
$this->addItemToUser($userId, $selectedConfig['item_id'], $itemQuantity);
// 记录奖励
$rewards[] = [
'item_id' => $item->id,
'name' => $item->name,
'quantity' => $itemQuantity,
'icon' => $item->icon
];
// 如果该物品不允许重复掉落,则从可选列表中移除
if (!$selectedConfig['allow_duplicate'] ?? false) {
array_splice($availableConfigs, $selectedIndex, 1);
}
}
}
DB::commit();
return [
'success' => true,
'chests' => [
[
'chest_id' => $chestId,
'chest_name' => $chest->name,
'rewards' => $rewards
]
],
'remaining_chests' => $userChest->quantity ?? 0
];
} catch (\Exception $e) {
DB::rollBack();
return [
'success' => false,
'message' => '宝箱打开失败: ' . $e->getMessage()
];
}
}
物品模块支持两种过期时间机制:
全局过期时间定义在items表的global_expire_at字段中,表示该物品在所有用户中的绝对过期时间。当超过这个时间点后,所有用户的该物品均失效。
应用场景:
用户物品过期时间定义在user_items表的expire_at字段中,表示特定用户的特定物品的过期时间。这允许每个用户的同一物品有不同的过期时间。
应用场景:
系统应定期运行任务检查并处理过期物品:
// 计划任务中
public function handleExpiredItems()
{
// 处理全局过期物品
$expiredGlobalItems = Item::where('global_expire_at', '<', now())
->whereNotNull('global_expire_at')
->get();
foreach ($expiredGlobalItems as $item) {
// 删除所有用户的该物品或标记为失效
UserItem::where('item_id', $item->id)->delete();
// 可选操作:发送通知给用户
}
// 处理用户物品过期
$expiredUserItems = UserItem::where('expire_at', '<', now())
->whereNotNull('expire_at')
->get();
foreach ($expiredUserItems as $userItem) {
// 删除过期的用户物品
$userItem->delete();
// 可选操作:发送通知给用户
}
}
宝箱可以根据稀有度和内容分为不同类型:
每个宝箱可以同时掉落多个物品:
items表中定义了min_drop_count和max_drop_count字段chest_items表中的allow_duplicate字段控制物品是否可以在同一宝箱中重复掉落宝箱内容的概率由chest_items表中的weight字段决定:
每次宝箱开启都应记录详细日志,包括:
这些日志可用于数据分析和问题排查。