初始化提交

This commit is contained in:
2025-07-09 01:08:35 +08:00
parent d3296791cf
commit 62e0f56c60
618 changed files with 173543 additions and 0 deletions

View File

@ -0,0 +1,27 @@
local Ability = {}
local GetGameplayTag = require("GamePlay.Utils").GetGameplayTag
local AbilitySystemBlueprintLibrary = import("AbilitySystemBlueprintLibrary")
function Ability:K2_ActivateAbilityFromEvent(_)
local RoleUtils = require("GamePlay.Utils.RoleUtils")
local owner = self:GetOwningActorFromActorInfo()
local asc = AbilitySystemBlueprintLibrary.GetAbilitySystemComponent(owner)
local increase_handle = asc:MakeOutgoingSpec(
self.DefaultEffectConfig, 1, asc:MakeEffectContext()
)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
increase_handle, GetGameplayTag("Change.Role.MoveSpeed"), self.SpeedIncrease
)
asc:BP_ApplyGameplayEffectSpecToSelf(increase_handle)
RoleUtils.ChangeHunger(owner, -100)
end
-- function Ability:K2_OnEndAbility(bWasCancelled)
-- print(bWasCancelled, "Ability:K2_OnEndAbility")
-- end
return Class(nil, nil, Ability)

View File

@ -0,0 +1,29 @@
local EatFoodAbility = {}
local Utils = require("GamePlay.Utils")
local RoleUtils = require("GamePlay.Utils.RoleUtils")
local function HandleHealthChange(role, effects)
local tag = Utils.GetGameplayTag("Change.Role.Health")
local value = effects:Get(tag)
if value ~= nil then
RoleUtils.ChangeHealth(role, value)
end
end
local function HandleHungerChange(role, effects)
local tag = Utils.GetGameplayTag("Change.Role.Hunger")
local value = effects:Get(tag)
if value ~= nil then
RoleUtils.ChangeHunger(role, value)
end
end
function EatFoodAbility:K2_ActivateAbilityFromEvent(EventData)
local item_config = Utils.GetItemConfigByID(math.floor(EventData.EventMagnitude))
local owner = self:GetOwningActorFromActorInfo()
HandleHealthChange(owner, item_config.GameplayEffects)
HandleHungerChange(owner, item_config.GameplayEffects)
self:K2_EndAbility()
end
return Class(nil, nil, EatFoodAbility)

View File

@ -0,0 +1,94 @@
local Ability = {}
local EBusyRoleState = import("EBusyRoleState")
local ERoleMoveDirection = import("ERoleMoveDirection")
local EBusyAnimationPhase = import("EBusyAnimationPhase")
local KismetSystemLibrary = import("KismetSystemLibrary")
local AbilitySystemBlueprintLibrary = import("AbilitySystemBlueprintLibrary")
local GetGameplayTag = require("GamePlay.Utils").GetGameplayTag
local item_pick_tag = "Change.LevelItem.Health"
local role_pick_cost_health_tag = "Change.Role.Health"
local role_pick_cost_hunger_tag = "Change.Role.Hunger"
local function GetSelfPickConsume(item_id)
end
function Ability:ctor()
self.target = nil
self.pick_phase = nil
self.delay_timer = nil
self.event_data = nil
end
function Ability:K2_ActivateAbilityFromEvent(EventData)
print("Pick Ability:K2_ActivateAbility")
self.pick_phase = EBusyAnimationPhase.PrepareCast
self:ProcessAbilityPhase()
self.event_data = EventData
self.target = EventData.Target
end
function Ability:K2_OnEndAbility()
local owner = self:GetOwningActorFromActorInfo()
if self.target:IsAlive() then
owner:TryActiveAbility("Ability.Role.Pick", self.event_data)
else
owner.proxy.state = EBusyRoleState.PickFinished
owner:UpdateRoleState()
end
end
function Ability:ProcessAbilityPhase()
local owner = self:GetOwningActorFromActorInfo()
local Animation = owner["RoleAnimation"]
Animation:PlayPickAnimation("Tree", Animation:GetMoveDirection(), self.pick_phase, 1.0)
if self.delay_timer ~= nil then
KismetSystemLibrary.K2_ClearTimerHandle(self, self.delay_timer)
end
if self.pick_phase == EBusyAnimationPhase.PrepareCast then
local delegate = slua.createDelegate(function()
self.pick_phase = EBusyAnimationPhase.Casting
self:ProcessAbilityPhase()
end)
self.delay_timer = KismetSystemLibrary.K2_SetTimerDelegate(delegate, 0.5, false, true, 0, 0)
elseif self.pick_phase == EBusyAnimationPhase.Casting then
self.pick_phase = EBusyAnimationPhase.PostCast
self:ProcessAbilityPhase()
self:ApplayEffect(owner.RoleAbility)
else
local delegate = slua.createDelegate(function()
self:K2_EndAbility()
end)
self.delay_timer = KismetSystemLibrary.K2_SetTimerDelegate(delegate, 0.2, false, true, 0, 0)
end
end
function Ability:ApplayEffect(asc)
local RoleUtils = require("GamePlay.Utils.RoleUtils")
local owner = self:GetOwningActorFromActorInfo()
-- 物品的采集效果
local item_asc = AbilitySystemBlueprintLibrary.GetAbilitySystemComponent(self.target)
local pick_handle = asc:MakeOutgoingSpec(
self.AbilityEffectConfigs:Get("LevelItem"), 1, asc:MakeEffectContext()
)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
pick_handle, GetGameplayTag(item_pick_tag), -owner.RoleConfig.PickEffect
)
-- 自己的消耗
RoleUtils.ChangeHunger(owner,-self.target.LevelItemConfig.PickHungerCost)
asc:BP_ApplyGameplayEffectSpecToTarget(pick_handle, item_asc)
end
return Class(nil, nil, Ability)

View File

@ -0,0 +1,26 @@
local Ability = {}
local GetGameplayTag = require("GamePlay.Utils").GetGameplayTag
local BlueprintGameplayTagLibrary = import("BlueprintGameplayTagLibrary")
local AbilitySystemBlueprintLibrary = import("AbilitySystemBlueprintLibrary")
function Ability:K2_ActivateAbilityFromEvent(EventData)
local tag = EventData.tag
local value = EventData.EventMagnitude
local asc = AbilitySystemBlueprintLibrary.GetAbilitySystemComponent(self.target)
if tag == "Recover.Role.Health" then
local spec_handle = asc:MakeOutgoingSpec(
self.AbilityEffectConfigs:Get("Role"), 1, asc:MakeEffectContext()
)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
spec_handle, GetGameplayTag("Change.Role.Health"), value
)
asc:BP_ApplyGameplayEffectSpecToSelf(spec_handle)
end
end
return Class(nil, nil, Ability)

View File

@ -0,0 +1,71 @@
local Ability = {}
local Reactive = require("Core.Reactive")
local GetGameplayTag = require("GamePlay.Utils").GetGameplayTag
local AbilitySystemBlueprintLibrary = import("AbilitySystemBlueprintLibrary")
local hunger_consume_tag_name = "Buff.RoleConsume.Hunger"
local health_consume_tag_name = "Buff.RoleConsume.Health"
function Ability:K2_ActivateAbilityFromEvent(_)
print("Role Consume Ability:K2_ActivateAbility")
if self.hunger_watcher ~= nil then
self.hunger_watcher:Destroy()
end
self.hunger_watcher = Reactive.Watcher(function() self:ConsumeWatcher() end)
end
function Ability:ConsumeWatcher()
local owner = self:GetOwningActorFromActorInfo()
local asc = owner["RoleAbility"]
if owner.LuaRoleAttribute.Hunger > 0 then
if not asc:IsGameplayCueActive(GetGameplayTag(hunger_consume_tag_name)) then
self:ApplyHungerConsume(asc)
end
if asc:IsGameplayCueActive(GetGameplayTag(health_consume_tag_name)) then
if self.health_consume_handle ~= nil then
asc:RemoveActiveGameplayEffect(self.health_consume_handle, -1)
end
self.health_consume_handle = nil
end
elseif owner.LuaRoleAttribute.Hunger <= 0 then
if not asc:IsGameplayCueActive(GetGameplayTag(health_consume_tag_name)) then
self:ApplyHealthConsume(asc)
end
if asc:IsGameplayCueActive(GetGameplayTag(hunger_consume_tag_name)) then
if self.hunger_consume_handle ~= nil then
asc:RemoveActiveGameplayEffect(self.hunger_consume_handle, -1)
end
self.hunger_consume_handle = nil
owner.LuaRoleAttribute.Hunger = 0
end
end
end
function Ability:ApplyConsumeEffect(asc, effect_tag_name, value)
local spec_handle = asc:MakeOutgoingSpec(
self.EffectConfigs:Get(GetGameplayTag(effect_tag_name)),
1, asc:MakeEffectContext()
)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
spec_handle, GetGameplayTag(effect_tag_name), value
)
return asc:BP_ApplyGameplayEffectSpecToSelf(spec_handle)
end
function Ability:ApplyHungerConsume(asc)
local owner = asc:GetOwner()
local consume_speed_peer_second = owner.RoleConfig.HungerConsumeSpeed
self.hunger_consume_handle = self:ApplyConsumeEffect(
asc, hunger_consume_tag_name, -consume_speed_peer_second / 10
)
end
function Ability:ApplyHealthConsume(asc)
local owner = asc:GetOwner()
local consume_speed_peer_second = owner.RoleConfig.HungerConsumeSpeed
self.health_consume_handle = self:ApplyConsumeEffect(
asc, health_consume_tag_name, -consume_speed_peer_second/10
)
end
return Class(nil, nil, Ability)

View File

@ -0,0 +1,21 @@
local Bonfire = {}
local GamePlayUtils = require("GamePlay.Utils")
local Utils = require("GamePlay.Utils")
local item_effect_health_tag = "Change.Role.Health"
local item_effect_hunger_tag = "Change.Role.Health"
function Bonfire:ctor()
end
function Bonfire:ReceiveBeginPlay()
self.Inventory:SetInventoryCapacity(20)
end
function Bonfire:StoreItem(item_id)
return self.Inventory:DepositItems(item_id, 1)
end
return Class(nil, nil, Bonfire)

View File

@ -0,0 +1,108 @@
-- 禁用不必要的if诊断警告
---@diagnostic disable: unnecessary-if
-- 导入基类模块
local PWClass = require("Core.PWClass")
--- @class StorageClass
--- @field max_capacity number 最大容量(格子总数)
--- @field cur_capacity number 当前已用容量
--- @field grids_list table[] 存储格子列表
local StorageClass = PWClass.derive("StorageClass")
--- 创建新物品格子
--- @param item_id any 物品唯一标识
--- @return table 新创建的物品格子
local function CreateGrid(item_id)
return {
item_id = item_id, -- 物品ID
cur_cnt = 0, -- 当前数量
max_cnt = 1 -- 最大堆叠数(可扩展为配置项)
}
end
--- 查找或创建可用物品格子
--- @param storage StorageClass 存储实例
--- @param item_id any 目标物品ID
--- @return table 可用格子(找不到时创建新格子)
local function FindOrCreateAvailableGrid(storage, item_id)
-- 优先查找同类型且未满的格子
for _, grid in ipairs(storage.grids_list) do
if grid.item_id == item_id and grid.cur_cnt < grid.max_cnt then
return grid
end
end
-- 无可用格子时创建新的物品类型格子
local new_grid = CreateGrid(item_id)
table.insert(storage.grids_list, new_grid)
return new_grid
end
--- 构造函数
function StorageClass:ctor()
self.max_capacity = 10 -- 默认最大容量
self.cur_capacity = 0 -- 当前使用容量
self.grids_list = {} -- 格子容器
end
--- 设置存储容量上限
--- @param capacity number 新的最大容量
function StorageClass:SetMaxCapacity(capacity)
self.max_capacity = capacity
end
--- 存储物品
--- @param item_id any 要存储的物品ID
function StorageClass:Store(item_id)
-- 容量检查
if self.cur_capacity >= self.max_capacity then
return false -- 建议返回操作结果
end
local grid = FindOrCreateAvailableGrid(self, item_id)
grid.cur_cnt = grid.cur_cnt + 1
self.cur_capacity = self.cur_capacity + 1
return true -- 建议添加返回值
end
--- 取出物品
--- @param item_id any 目标物品ID
--- @return boolean 是否成功取出
function StorageClass:Withdraw(item_id)
-- 逆序遍历提高取出效率(通常新物品在末尾)
for i = #self.grids_list, 1, -1 do
local grid = self.grids_list[i]
if grid ~= nil and grid.item_id == item_id and grid.cur_cnt > 0 then
grid.cur_cnt = grid.cur_cnt - 1
self.cur_capacity = self.cur_capacity - 1
-- 清空空格子
if grid.cur_cnt == 0 then
table.remove(self.grids_list, i)
end
return true
end
end
return false
end
function StorageClass:Visit(vistor)
for _, grid in ipairs(self.grids_list) do vistor(_, grid) end
end
--- 查询物品(满足条件的物品及数量)
--- @param query_function fun(item_id:any):boolean 物品过滤函数
--- @return table 物品ID到数量的映射表
function StorageClass:QueryItem(query_function)
local items = {}
for _, grid in ipairs(self.grids_list) do
-- 仅统计有物品且满足查询条件的格子
if grid.cur_cnt > 0 and query_function(grid.item_id) then
items[grid.item_id] = (items[grid.item_id] or 0) + grid.cur_cnt
end
end
return items
end
return StorageClass

View File

@ -0,0 +1,18 @@
local PlayerState = {}
function PlayerState:ctor()
end
function PlayerState:ReceiveBeginPlay()
print(self, "PlayerState:ReceiveBeginPlay")
end
function PlayerState:ReceiveEndPlay()
print(self, "PlayerState:ReceiveEndPlay")
end
return Class(nil, nil, PlayerState)

View File

@ -0,0 +1,68 @@
local Animation = {}
local ERoleState = import("EBusyRoleState")
local ERoleMoveDirection = import("ERoleMoveDirection")
local Reactive = require("Core.Reactive")
local direction_mapping = {
[0] = ERoleMoveDirection.Move_Right,
[1] = ERoleMoveDirection.Move_Left,
}
local function GetAnimationIndex(direction, N)
-- 使用自定义 atan2 计算角度
local angle = math.atan(direction.Y, direction.X)
-- 转换为 [0, 2π) 范围
if angle < 0 then angle = angle + 2 * math.pi end
-- 调整角度:使 Y 轴负方向 (0,-1) 成为 0°第一份的中心
local adjusted_angle = (angle + math.pi / N) % (2 * math.pi)
-- local adjusted_angle = (angle + math.pi / 2 + math.pi / N) % (2 * math.pi)
-- 计算每份的角度大小
local theta = 2 * math.pi / N
-- -- 计算所属区间的索引1-based
local index = math.floor(adjusted_angle / theta)
-- 处理边界情况adjusted_angle = 2π 时归到第 1 份)
if index > N then index = 0 end
return index
end
function Animation:ReceiveLuaBeginPlay()
self.prev_direction = nil
self.watcher = Reactive.Watcher(function() self:UpdateMoveAnimation() end)
end
function Animation:UpdateMoveAnimation()
local owner = self:GetOwner()
local movement_proxy = owner.Movement.proxy
local index = 0
if owner.proxy.state == ERoleState.Picking then
return
end
if movement_proxy.isIdle then
if self.prev_direction ~= nil then
index = GetAnimationIndex(self.prev_direction, ERoleMoveDirection.Move_All_Cnt)
end
self:SetIdleAnimation(direction_mapping[index])
else
local direction = movement_proxy.direction
if direction.X ~= 0 and direction.Y ~= 0 then
index = GetAnimationIndex(direction, ERoleMoveDirection.Move_All_Cnt)
end
self:SetMoveAnimation(direction_mapping[index])
self.prev_direction = {X = direction.X, Y = direction.Y}
end
end
function Animation:GetMoveDirection()
local index = GetAnimationIndex(self.prev_direction, ERoleMoveDirection.Move_All_Cnt)
return direction_mapping[index]
end
return Class(nil, nil, Animation)

View File

@ -0,0 +1,78 @@
local FVector = import "Vector"
local FVector2D = import "Vector2D"
local Reactive = require "Core.Reactive"
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem")
local Movement = {
proxy = {},
}
function Movement:ReceiveComponentBeginPlay()
self.proxy = Reactive.ReactiveProperty({
isIdle = true,
direction = {X = 0, Y = 0},
destination = {X = 0, Y = 0}
})
end
function Movement:ReceiveComponentTick(delta_time)
local proxy = self.proxy
if proxy.isIdle then return end
local new_pos = FVector()
local owner = self:GetOwner()
local curr_pos = owner:K2_GetActorLocation()
new_pos.Z = curr_pos.Z
local move_distance = delta_time * owner:GetSpeed()
local distance_x = proxy.destination.X - curr_pos.X
local distance_y = proxy.destination.Y - curr_pos.Y
if move_distance^2 >= distance_x^2 + distance_y^2 then
proxy.isIdle = true
new_pos.X = proxy.destination.X
new_pos.Y = proxy.destination.Y
proxy.direction = FVector2D()
else
new_pos.X = curr_pos.X + proxy.direction.X * move_distance
new_pos.Y = curr_pos.Y + proxy.direction.Y * move_distance
end
owner:K2_SetActorLocation(new_pos, true, nil, false)
end
function Movement:SetDestination(x, y)
local direction = FVector2D()
local destination = FVector2D()
local owner = self:GetOwner()
local curr_pos = owner:K2_GetActorLocation()
local delta_x = x - curr_pos.X
local delta_y = y - curr_pos.Y
-- 计算方向向量
local length = math.sqrt(delta_x ^ 2 + delta_y ^ 2)
if length > 0 then
direction.X, direction.Y = delta_x / length, delta_y / length
else
direction.X, direction.Y = 0, 0
end
destination.X , destination.Y = x, y
self.proxy.isIdle = false
self.proxy.direction = direction
self.proxy.destination = destination
end
function Movement:Stop()
self.proxy.isIdle = true
end
function Movement:BackBonfire()
local sub_system = BusyActorManagerSubSystem.Get(self)
local bonfire = sub_system:GetNearestBonfire()
local position = bonfire:K2_GetActorLocation()
self:SetDestination(position.X, position.Y)
end
return Class(nil, nil, Movement)

View File

@ -0,0 +1,15 @@
local InventoryComponent = {}
function InventoryComponent:ReceiveBeginPlay()
print(self, "InventoryComponent:ReceiveBeginPlay")
end
function InventoryComponent:InitItemGrid(grid)
grid.MaxCount = 1
grid.CurrentCount = 0
grid.Priority = 1
return grid
end
return Class(nil, nil, InventoryComponent)

View File

@ -0,0 +1,59 @@
local Movement = {}
local RoleUtils = require("GamePlay.Utils.RoleUtils")
local Vector2D = require("Utils.Vector2D")
function Movement:ctor()
self.speed = 300.0
self.accelerate = 300.0
self.rate = 0.0
self.direction = Vector2D.New()
end
function Movement:ReceiveBeginPlay()
self.rate = 0.0
self.direction = Vector2D.Normalize(self.direction)
end
function Movement:SetMoveOriginDirection(vector)
self.direction = Vector2D.Normalize(vector)
end
function Movement:SetMoveOriginSpeed(speed, accelerate)
self.speed = speed
self.accelerate = accelerate
end
function Movement:ReceiveTick(DeltaSeconds)
local owner = self:GetOwner()
local role = RoleUtils.GetRole(self)
local role_location = role:K2_GetActorLocation()
local curr_location = owner:K2_GetActorLocation()
local accelerate_vector = Vector2D.New(
role_location.X - curr_location.X,
role_location.Y - curr_location.Y
)
accelerate_vector = Vector2D.Normalize(accelerate_vector)
self.rate = self.rate + DeltaSeconds * 0.1
local velocity_vector = Vector2D.Mul(self.direction, self.speed)
local direction_vector= Vector2D.Mul(accelerate_vector, self.speed * self.rate)
self.direction = Vector2D.Normalize(Vector2D.Add(velocity_vector, direction_vector))
local new_velocity = Vector2D.Mul(self.direction, self.speed)
local new_location = Vector2D.New(
curr_location.X + new_velocity.X * DeltaSeconds,
curr_location.Y + new_velocity.Y * DeltaSeconds
)
self.speed = self.speed + self.accelerate * DeltaSeconds
owner:K2_SetActorLocation(
Vector2D.ToUnrealEngine3D(new_location, curr_location.Z),
true, nil, false
)
end
function Movement:ReceiveEndPlay()
print("Movement:ReceiveEndPlay 123")
end
return Class(nil, nil, Movement)

View File

@ -0,0 +1,112 @@
local FVector = import "Vector"
local Vector2D = require("Utils.Vector2D")
local Library = import "BusyGamePlayLibrary"
local GameplayStatics = import("GameplayStatics")
local SubSystem = {}
local function GetNearestBonfire(system, x, y)
local selected_bonfire = nil
local selected_distance = nil
for _, bonfire in ipairs(system.bonfire_list) do
local pos = bonfire:K2_GetActorLocation()
local distance = (x - pos.X) ^ 2 + (y - pos.Y) ^ 2
if selected_distance == nil or distance < selected_distance then
selected_distance = distance
selected_bonfire = bonfire
end
end
return selected_bonfire
end
function SubSystem:ctor()
self.current_role = nil
self.bonfire_list = {} -- 所有的篝火列表
end
function SubSystem:ReceiveSubSystemInitialize()
self.current_role = nil
self.bonfire_list = {}
end
function SubSystem:GetNearestBonfire()
if self.current_role then
local cur_pos = self.current_role:K2_GetActorLocation()
return GetNearestBonfire(self, cur_pos.X, cur_pos.Y)
end
return nil
end
function SubSystem:SpawnBonfire(position)
local pos = FVector()
local world = self:K2_GetWorld()
local cls = Library.GetGameClass("Bonfire")
pos.X, pos.Y, pos.Z = position.X, position.Y, 20
local bonfire = world:SpawnActor(cls, pos, nil, nil)
table.insert(self.bonfire_list, bonfire)
return bonfire
end
function SubSystem:SpawnRole(bonfire)
local role_pos = FVector()
local world = self:K2_GetWorld()
local pos = bonfire:K2_GetActorLocation()
local cls = Library.GetGameClass("BusyRole")
role_pos.X, role_pos.Y, role_pos.Z = pos.X, pos.Y, pos.Z + 10
self.current_role = world:SpawnActor(cls, role_pos, nil, nil)
if self.current_role ~= nil then
self.current_role:SetRole("Rabbit")
return self.current_role
else
return nil
end
end
function SubSystem:SpawnLevelItem(item_id)
-- 随机在角色周围生成
local distance = math.random(128, 500)
local angle = (math.random(0, 360) / 360) * 2 * 3.14;
local world = self:K2_GetWorld()
local item_position = FVector()
local center = self.current_role:K2_GetActorLocation()
local cls = import("BusyLevelItem")
item_position.Z = center.Z - 1
item_position.X = center.X + math.cos(angle) * distance
item_position.Y = center.Y + math.sin(angle) * distance
local item = world:SpawnActor(cls, item_position, nil, nil)
item:SetLevelItemID(item_id)
return center
end
function SubSystem:SpawnLevelItemReward(level_item)
assert(self.current_role ~= nil)
local world = self:K2_GetWorld()
local cls = Library.GetGameClass("LevelItemReward")
local random_angle = (math.random() - 0.5) * (math.pi / 2)
local direction = Vector2D.Normalize(self.current_role:GetMoveDirection())
local sin, cos = math.sin(random_angle), math.cos(random_angle)
-- 应用旋转矩阵
direction.X = direction.X * cos - direction.Y * sin
direction.Y = direction.X * sin + direction.Y * cos
local item_location = level_item:K2_GetActorLocation()
local reward_location = Vector2D.Add(item_location, Vector2D.Mul(direction, 200))
local item = world:SpawnActor(cls,
Vector2D.ToUnrealEngine3D(reward_location, item_location.Z),
nil, nil
)
return item
end
return Class(nil, nil, SubSystem)

View File

@ -0,0 +1,119 @@
---@class ItemGenerator 物品生成器类
local ItemGenerator = {
item_data = {},
period = 100,
item_distributions = {}
}
ItemGenerator.__index = ItemGenerator
---数组洗牌函数Fisher-Yates算法
---@param array any[] 需要打乱顺序的数组
local function ShuffleArray(array)
for i = #array, 2, -1 do -- 从后往前遍历
local j = math.random(i) -- 生成1到i的随机数
array[i], array[j] = array[j], array[i] -- 交换元素位置
end
end
---创建物品生成器实例
---@param period number 生成周期次数
---@param item_data table<number, number> 物品配置表 {物品ID = 总生成数量}
---@return ItemGenerator 物品生成器实例
local function CreateItemGenerator(period, item_data)
local self = setmetatable({}, ItemGenerator)
---@type number 存储生成周期
self.period = period
---@type table<number, number> 原始配置数据
self.item_data = item_data
---@type number 当前调用次数
self.current_call = 0
---@type table<number, number[]> 物品分布数据结构
self.item_distributions = {}
self:InitializeDistributions()
return self
end
---初始化物品分布数据
function ItemGenerator:InitializeDistributions()
-- 遍历所有物品配置
for item_id, total in pairs(self.item_data) do
-- 计算基础值和余数(保证总数 = 基础值*period + 余数)
local base = math.floor(total / self.period)
local remainder = total % self.period
-- 创建初始分布数组(全部填充基础值)
local distribution = {}
for i = 1, self.period do
distribution[i] = base
end
-- 生成索引数组并洗牌(用于随机分配余数)
local indices = {}
for i = 1, self.period do
indices[i] = i
end
ShuffleArray(indices) -- 打乱索引顺序
-- 将余数随机分配到前remainder个位置
for i = 1, remainder do
distribution[indices[i]] = distribution[indices[i]] + 1
end
-- 存储当前物品的分布数组
self.item_distributions[item_id] = distribution
end
end
---重置生成器状态当调用次数超过N时触发
function ItemGenerator:Reinitialize()
self:InitializeDistributions() -- 重新生成分布数据
self.current_call = 0 -- 重置调用计数器
end
---生成物品列表(每次调用产生一个周期的结果)
---@return number[] 包含物品ID的数组结果经过随机排序
function ItemGenerator:Generate()
-- 当超过周期次数时重置状态
if self.current_call >= self.period then
self:Reinitialize()
end
local current_step = self.current_call + 1 -- 获取当前步骤1-based
local result = {} -- 结果收集器
-- 遍历所有物品的分布数据
for item_id, distribution in pairs(self.item_distributions) do
local count = distribution[current_step] -- 获取当前步骤应生成数量
for _ = 1, count do
table.insert(result, item_id) -- 按数量添加物品ID到结果集
end
end
ShuffleArray(result) -- 打乱结果顺序保证随机性
self.current_call = self.current_call + 1 -- 增加调用计数器
return result
end
---获取当前未生成的物品剩余数量
---@return table<number, number> 返回物品ID和剩余数量的映射表
function ItemGenerator:GetRemainingItems()
local remaining = {}
local current_step = self.current_call -- 注意这里使用已调用次数0-based
-- 遍历所有物品的分布数据
for item_id, distribution in pairs(self.item_distributions) do
local total_remaining = 0
-- 计算从下一个步骤到周期结束的总数量
for step = current_step + 1, self.period do
total_remaining = total_remaining + distribution[step]
end
remaining[item_id] = total_remaining
end
return remaining
end
return CreateItemGenerator

View File

@ -0,0 +1,69 @@
local SubSystem = {}
local Reactive = require("Core.Reactive")
local Library = import("BusyGamePlayLibrary")
local GameplayStatics = import("GameplayStatics")
local BusyGamePlayLibrary = import("BusyGamePlayLibrary")
local function CreateItemGenerator(level_config_data)
local Generator = require("GamePlay.Level.BusyLevelItemGenerator")
local item_data = {}
local period = level_config_data.Period
for k, v in pairs(level_config_data.LevelItemIds) do
item_data[k] = v.CountOfPeriod
end
return Generator(period, item_data)
end
function SubSystem:ReceiveSubSystemInitialize()
local world = BusyGamePlayLibrary.K2_GetWorld(self)
self.start_time = GameplayStatics.GetTimeSeconds(world)
self.proxy = Reactive.ReactiveProperty({
current_seconds = 0
})
end
function SubSystem:ReceiveWorldBeginPlay()
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem").Get(self)
-- 读取关卡配置
local is_suc, row_data = Library.GetLevelBaseConfig("Default", nil)
assert(is_suc == true, "Can't find level base config")
self.level_base_config = row_data
-- 创建物品生成器
self.generator = CreateItemGenerator(row_data)
-- 创建初始篝火
local bonfire = BusyActorManagerSubSystem:SpawnBonfire(row_data.FirstBonfirePosition)
-- 创建角色
local role = BusyActorManagerSubSystem:SpawnRole(bonfire)
GameplayStatics.GetPlayerController(self, 0):Possess(role)
end
function SubSystem:ReceiveSubSystemTick(DeltaTime)
-- local proxy = self.proxy
-- local world = BusyGamePlayLibrary.K2_GetWorld(self)
-- local current_time = GameplayStatics.GetTimeSeconds(world)
-- local escapse_time = math.floor(current_time - self.start_time)
-- if escapse_time > proxy.current_seconds then
-- self:TrySpawnLevelItem()
-- proxy.current_seconds = escapse_time
-- print(proxy.current_seconds)
-- end
end
function SubSystem:TrySpawnLevelItem()
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem").Get(self)
local items = self.generator:Generate()
for i, item_id in pairs(items) do
BusyActorManagerSubSystem:SpawnLevelItem(item_id)
print(i, item_id)
end
end
return Class(nil, nil, SubSystem)

View File

@ -0,0 +1,125 @@
--- @class BusyLevelItem
local LevelItem = {}
local Reactive = require("Core.Reactive")
local RoleUtils = require("GamePlay.Utils.RoleUtils")
local GameplayUtils = require("GamePlay.Utils")
local KismetSystemLibrary = import("KismetSystemLibrary")
--[[
Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_CallFunction_0" ExportPath="/Script/BlueprintGraph.K2Node_CallFunction'/Game/Blueprint/Bp_BusyCharacter.Bp_BusyCharacter:EventGraph.K2Node_CallFunction_0'"
bIsPureFunc=True
FunctionReference=(MemberParent="/Script/CoreUObject.Class'/Script/Engine.KismetSystemLibrary'",MemberName="IsValid")
NodePosX=416
NodePosY=576
NodeGuid=FAEC35AE4921EA704624228C12C6567F
CustomProperties Pin (PinId=560B26AF426C21B8AC88EDB90990EDF1,PinName="self",PinFriendlyName=NSLOCTEXT("K2Node", "Target", "Target"),PinToolTip="Target\nKismet System Library Object Reference",PinType.PinCategory="object",PinType.PinSubCategory="",PinType.PinSubCategoryObject="/Script/CoreUObject.Class'/Script/Engine.KismetSystemLibrary'",PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,DefaultObject="/Script/Engine.Default__KismetSystemLibrary",PersistentGuid=00000000000000000000000000000000,bHidden=True,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
CustomProperties Pin (PinId=5F3C3BC8429FA25D9B78099F2107C098,PinName="Object",PinToolTip="Object\nObject Reference",PinType.PinCategory="object",PinType.PinSubCategory="",PinType.PinSubCategoryObject="/Script/CoreUObject.Class'/Script/CoreUObject.Object'",PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=True,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
CustomProperties Pin (PinId=5C7188354FCB6B61DE0AC8A14D6E3EEC,PinName="ReturnValue",PinToolTip="Return Value\nBoolean\n\nReturn true if the object is usable : non-null and not pending kill",Direction="EGPD_Output",PinType.PinCategory="bool",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,DefaultValue="false",AutogeneratedDefaultValue="false",PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
End Object
]]
function LevelItem:ReceiveBeginPlay()
self.Super:ReceiveBeginPlay()
self:SetLevelItemID(self.CurrentItemID)
self.world = self:GetWorld()
end
function LevelItem:ReceiveLevelItemSetted(Config)
self.config = Config
self.PickBar.Widget:BindLevelItem(self)
print("LevelItem:ReceiveLevelItemSetted", KismetSystemLibrary.IsValid(self))
self.attribute_watcher = Reactive.Watcher(function()
if self.LuaLevelItemAttribute.Health <= 0 then
self:GenerateDropItems()
local location = self:K2_GetActorLocation()
location.Z = -1000
self:K2_SetActorLocation(location, false, nil, false)
self:SetLifeSpan(0.3)
self.attribute_watcher:Destroy()
end
end)
end
-- function LevelItem:ReceiveDamage(value)
-- local remain = self.proxy.RemainProcess - value
-- if remain > 0 then
-- self.proxy.RemainProcess = remain
-- return true
-- else
-- self.proxy.RemainProcess = 0
-- self:SetLifeSpan(0.3)
-- return false
-- end
-- end
function LevelItem:GenerateDropItems()
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem").Get(self.world)
local role = RoleUtils.GetRole(self)
local role_location = role:K2_GetActorLocation()
local curr_location = self:K2_GetActorLocation()
local collection_config = GameplayUtils.GetItemConfigByID(self.CurrentItemID)
local generated_dict = {}
for _, config in pairs(collection_config.DropConfigs) do
local base = 0
local seed = math.random()
for _, rate in pairs(config.configs) do
local min = base
local max = base + rate.Rate
if seed >= min and seed < max then
generated_dict[config.ItemID] = rate.Count
break
end
base = max
end
end
for item_id, count in pairs(generated_dict) do
for _ = 1, count do
local reward = BusyActorManagerSubSystem:SpawnLevelItemReward(self)
reward.Movement.speed = 300.0
reward.Movement.accelerate = 600.0
reward.Movement.direction = {
X = curr_location.X - role_location.X,
Y = curr_location.Y - role_location.Y
}
reward:SetRewardID(item_id)
end
end
end
-- 接口
function LevelItem:GetPickProcess()
local process = self.LuaLevelItemAttribute.Health / self.config.PickTimeCost
print("current process", process)
return process
end
function LevelItem:GetPickCost()
return self.LevelItemConfig.PickHungerCost
end
function LevelItem:IsAlive()
return self.LuaLevelItemAttribute.Health > 0
end
function LevelItem:GetItemID()
return self.CurrentItemID
end
return Class(nil, nil, LevelItem)

View File

@ -0,0 +1,6 @@
local GameMode = {}
function GameMode:ReceiveBeginPlay()
require("Debugger.LuaPanda").start("127.0.0.1",8818)
self.Super:ReceiveBeginPlay()
end
return Class(nil, nil, GameMode)

View File

@ -0,0 +1,24 @@
local PlayerController = {}
local KismetSystemLibrary = import("KismetSystemLibrary")
function PlayerController:convertCursorToWorldPosition()
-- 将当前鼠标的位置转化为世界的位置
local FVector = import("Vector")
local WorldOrigin, WorldDirection = FVector(), FVector()
local _, MouseX, MouseY = self:GetMousePosition(nil, nil)
if self:DeprojectScreenPositionToWorld(
MouseX, MouseY, WorldOrigin, WorldDirection
)then
return WorldOrigin.X, WorldOrigin.Y
else
return nil, nil
end
end
function PlayerController:QuitGame()
KismetSystemLibrary.QuitGame(self, self, 0, false)
end
return Class(nil, nil, PlayerController)

View File

@ -0,0 +1,203 @@
local UIUtils = require("UI.Utils")
local Reactive = require("Core.Reactive")
local ERoleState = import("EBusyRoleState")
local EBusyItemEffectType = import("EBusyItemEffectType")
local GameplayStatics = import ("GameplayStatics")
local GetGameplayTag = require("GamePlay.Utils").GetGameplayTag
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem")
local GamePlayUtils = require("GamePlay.Utils")
local KismetMathLibrary = import("KismetMathLibrary")
local LevelItemRewardClass = import("LevelItemReward")
--- @class BusyRole
local Role={
movement_watcher = nil,
interacte_item = nil
}
local PickAbilityTag = "Ability.Role.Pick"
local RecoverAbilityTag = "Ability.Role.Recover"
local ConsumeAbilityTag = "Ability.Role.AttributeConsume"
local RollAbilityTag = "Ability.Role.Roll"
-- 私有函数
-- 采集相关
--- @param role BusyRole
--- @param pick_targer BusyLevelItem
local function ResetPickWatcher(role, pick_targer)
if role.pick_watcher ~= nil then
role.pick_watcher:Destroy()
end
role.pick_watcher = Reactive.Watcher(function()
if not pick_targer:IsAlive() then
role.pick_watcher:Destroy()
role.pick_watcher = nil
role.carried_item_id = pick_targer:GetItemID()
end
end)
end
--- 存储正在搬运的物品
local function StoreCarriedItem(role)
-- if not role.carried_item_id then return end
-- local sub_system = BusyActorManagerSubSystem.Get(role)
-- local bonfire = sub_system:GetNearestBonfire()
-- if bonfire:StoreItem(role.carried_item_id) then
-- role.carried_item_id = nil
-- end
end
function Role:ctor()
self.pick_timer = nil
self.carried_item_id = nil
self.time_limited_tags = {}
self.pick_watcher = nil -- 监听正在采集物品的状态
self.proxy = Reactive.ReactiveProperty({
state = ERoleState.BonfireIdle,
is_alive = true,
})
end
function Role:ReceiveBeginPlay()
self.bCanEverTick = false
self.Inventory:DepositItems(200001, 2)
self.movement_watcher = Reactive.Watcher(function()
self:UpdateRoleState()
end)
end
function Role:ReceiveEndPlay()
print(self, "Role:ReceiveEndPlay")
end
function Role:ReceiveSetRole(_)
self:TryActiveAbility(ConsumeAbilityTag, nil)
UIUtils.ShowWidget(self, "RoleState", {role=self})
end
function Role:UpdateRoleState()
-- 在auto run中访问Reactive Property的值会导致多次调用有性能隐患
local role_state = Reactive.RawGet(self.proxy, "state")
local move_proxy = self.Movement.proxy
if move_proxy.isIdle then
if role_state == ERoleState.Searching or role_state == ERoleState.PickFinished then
self.proxy.state = ERoleState.BackBonfire
self.Movement:BackBonfire()
elseif role_state == ERoleState.BackBonfire then
-- StoreCarriedItem(self)
self.proxy.state = ERoleState.BonfireIdle
end
else
if role_state == ERoleState.BonfireIdle then
self.proxy.state = ERoleState.Searching
end
end
print("old", role_state, "new", Reactive.RawGet(self.proxy, "state"), move_proxy.isIdle)
end
function Role:StartPick(level_item)
local GameplayEventData = import("GameplayEventData")
local EventData = GameplayEventData()
EventData.Instigator = self
EventData.Target = level_item
self.proxy.state = ERoleState.Picking
self:TryActiveAbility(PickAbilityTag, EventData)
-- ResetPickWatcher(self, level_item)
end
function Role:OnTouch(_)
local role_proxy = self.proxy
if role_proxy.state == ERoleState.BonfireIdle then
local pc = GameplayStatics.GetPlayerController(self:GetWorld(), 0)
local world_x, world_y = pc:convertCursorToWorldPosition()
role_proxy.state = ERoleState.Searching
self.Movement:SetDestination(world_x, world_y)
else
print("nothing")
end
end
function Role:OnOverlapBegin(OverlappedComp, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult)
if KismetMathLibrary.ClassIsChildOf(OtherActor:GetClass(), LevelItemRewardClass) then
self.Inventory:DepositItems(OtherActor.RewardID, 1)
OtherActor:ConditionalBeginDestroy()
else
if self.proxy.state ~= ERoleState.Searching then return end
self.proxy.state = ERoleState.Picking
self.Movement:Stop()
self:StartPick(OtherActor)
end
end
--- 使用翻滚技能
function Role:UseRollSkill()
self:TryActiveAbility(RollAbilityTag, nil)
end
-- 接口
function Role:GetPickEffect()
return self.RoleConfig.PickEffect
end
function Role:GetHealthPercent()
local current_health = self.LuaRoleAttribute.Health or 0
return current_health / self.RoleConfig.Health
end
function Role:GetHungerPercent()
return self.LuaRoleAttribute.Hunger / self.RoleConfig.Hunger
end
function Role:GetHungerConsumeSpeed()
if self.time_limited_tags["SkillRole"] ~= nil then
return self.RoleConfig.HungerConsumeSpeed * 8
else
return self.RoleConfig.HungerConsumeSpeed
end
end
function Role:GetSpeed()
return self.LuaRoleAttribute.MoveSpeed
end
function Role:GetMoveDirection()
return self.Movement.proxy.direction
end
--- 返回即将消耗的饥饿值和生命值
function Role:CalcRealChangeByHunger(value)
local config = self.RoleConfig
local fixed_change, extra_health_need = 0, 0
local cost_rate = config.HealthConsumeSpeed / config.HungerConsumeSpeed
local remain_hunger = self.LuaRoleAttribute.Hunger + value
if remain_hunger < 0 then
fixed_change = remain_hunger
extra_health_need = remain_hunger * cost_rate
elseif remain_hunger > self.RoleConfig.Hunger then
fixed_change = remain_hunger - self.RoleConfig.Hunger
end
return value - fixed_change, self:CalcRealChangeByHealth(extra_health_need)
end
function Role:CalcRealChangeByHealth(value)
if value == 0 then return 0 end
local fixed_change = 0
local remain_health = self.LuaRoleAttribute.Health + value
if remain_health < 0 then
fixed_change = remain_health
elseif remain_health > self.RoleConfig.Health then
fixed_change = remain_health - self.RoleConfig.Health
end
return value - fixed_change
end
return Class(nil, nil, Role)

View File

@ -0,0 +1,37 @@
local _M = {}
local GameplayTag = import("GameplayTag")
local BusyGamePlayLibrary = import("BusyGamePlayLibrary")
local function GetConfig(func_name, config_id)
local state, config = BusyGamePlayLibrary[func_name](config_id, nil)
if state == true then
return config
else
return nil
end
end
function _M.GetItemConfigByID(item_id)
return GetConfig("GetLevelItemConfig", item_id)
end
function _M.GetItemResourceConfig(item_id)
return GetConfig("GetItemResourceConfig", item_id)
end
function _M.GetItemDescConfig(item_id)
return GetConfig("GetLevelItemDescription", item_id)
end
function _M.GetRoleConfigByID(role_id)
return GetConfig("GetRoleConfig", role_id)
end
function _M.GetGameplayTag(name)
local tag = GameplayTag(name)
tag.TagName = name
return tag
end
return _M

View File

@ -0,0 +1,29 @@
local BuildUtils = {}
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem")
local function BuildBonfire(wco)
local sub_system = BusyActorManagerSubSystem.Get(wco)
local role = sub_system.current_role
local bonfire = sub_system:GetNearestBonfire()
local role_pos = role:K2_GetActorLocation()
local bonfire_pos = bonfire:K2_GetActorLocation()
local distance = (role_pos.X - bonfire_pos.X)^2 + (role_pos.Y - bonfire_pos.Y)^2
if distance >= 90000 then
sub_system:SpawnBonfire(role_pos)
return true
end
return false
end
local build_mapping = {
[200001] = BuildBonfire
}
function BuildUtils.Build(wco, item_id)
local build_function = build_mapping[item_id]
return build_function and build_function(wco) or false
end
return BuildUtils

View File

@ -0,0 +1,58 @@
local _M = {}
local GameplayEventData = import("GameplayEventData")
local GetGameplayTag = require("GamePlay.Utils").GetGameplayTag
local BusyGamePlayLibrary = import("BusyGamePlayLibrary")
local BusyActorManagerSubSystem = import("BusyActorManagerSubSystem")
local AbilitySystemBlueprintLibrary = import("AbilitySystemBlueprintLibrary")
function _M.ChangeHealth(role, value)
local asc = AbilitySystemBlueprintLibrary.GetAbilitySystemComponent(role)
local ge = asc.AbilityEffectConfigs:Get("ChangeHealth")
local cost_handle = asc:MakeOutgoingSpec(ge, 1, asc:MakeEffectContext())
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
cost_handle, GetGameplayTag("Change.Role.Health"),
role:CalcRealChangeByHealth(value)
)
asc:BP_ApplyGameplayEffectSpecToSelf(cost_handle)
end
function _M.ChangeHunger(role, value)
local asc = AbilitySystemBlueprintLibrary.GetAbilitySystemComponent(role)
local ge = asc.AbilityEffectConfigs:Get("ChangeHunger")
local hunger_cost, health_cost = role:CalcRealChangeByHunger(value)
local cost_handle = asc:MakeOutgoingSpec(ge, 1, asc:MakeEffectContext())
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
cost_handle, GetGameplayTag("Change.Role.Health"), health_cost
)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
cost_handle, GetGameplayTag("Change.Role.Hunger"), hunger_cost
)
asc:BP_ApplyGameplayEffectSpecToSelf(cost_handle)
end
function _M.GetRole(wco)
local mgr = BusyActorManagerSubSystem.Get(wco)
return mgr.current_role
end
function _M.EatFood(wco, item_id)
local role = _M.GetRole(wco)
local EventData = GameplayEventData()
EventData.EventMagnitude = item_id
role:TryActiveAbility("Ability.Role.EatFood", EventData)
end
function _M.GetRoleConfig(role_id)
local data_table = BusyGamePlayLibrary.GetGameDataTable("RoleConfig")
local row_data = data_table:FindRow(role_id, "error in here")
print(row_data)
end
return _M