%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 运动行为相关接口
%%% 运动行为主要指围绕运动能量联系在一起的特殊运动行为，
%%% 不包括普通的移动，主要包括跳跃、闪现等动作。
%%%
%%% 关于运动能量的恢复与消耗：
%%% 运动能量由跳跃、闪现动作进行消耗，按时间自然恢复。
%%%
%%% 与客户端的同步：
%%% 运动能量自然恢复不通知客户端，由客户端自行模拟变化
%%% 在触发动作行为时才与客户端做数据同步，
%%% 服务器内部使用该值时按时间计算得到实际值
%%%
%%% 服务端内部数据的管理：
%%% 玩家进入场景时由角色身上同步到场景，
%%% 场景中数据只在玩家触发动作时同步回角色进程。
%%% 人物身上数据为冗余数据，主要用于辅助判断，
%%% 减少无效操作，同时负责在需要时同步给客户端。
%%% 准确数据状态由场景管理，因为涉及战斗无敌，
%%% 需要由场景进程管理才能保证不出现双进程同步问题。
%%%
%%% 注意：
%%% 跨节点间存在时间差异，但是一般通讯耗时不是非常高，
%%% 通常忽略通讯耗时的影响，所以不做跨节点通讯平均时间较正处理，
%%% 在另一节点同步数据过来时认为该数据就是本节点收到消息时刻的数据
%%% （为了各功能更精准处理这个问题，
%%% 后面可能会做全局的游戏服与跨服的时间校正管理，到时再修改）
%%%
%%% create at 2020-05-13
%%% @end
%%% -------------------------------------------------------
-module(motion_energy).
-export([
        notice_energy/1
        ,login_init/0
        ,to_scene_motion/1
        ,check_cost_energy/2
        ,check_async_energy/4
        ,sync_motion_energy/2
    ]).

-include("common.hrl").
-include("server.hrl").
-include("motion.hrl").
-include("scene.hrl").

-spec notice_energy(Player) -> NewPlayer when
    Player :: #player_status{}, NewPlayer :: #player_status{}.
%% 通知运动能量数据
notice_energy(Player = #player_status{sid = Sid, role_motion = RoleMotion}) ->
    %% 角色身上一份冗余数据，精准数据在场景进程
    %% 只在触发动作时由场景同步回角色进程
    NewRoleMotion = #role_motion{energy = NewEnergy} = calc_energy(RoleMotion),
    send_energy_msg(Sid, NewEnergy),
    Player#player_status{role_motion = NewRoleMotion}.

send_energy_msg(Sid, NewEnergy) ->
    %% ?INFO_TTY("notice energy: ~w", [NewEnergy]),
    {ok, BinData} = pt_130:write(13016, [NewEnergy, ?MOTION_ENERGY_MAX]),
    lib_server_send:send_to_sid(Sid, BinData).

calc_energy(SceneMotion = #scene_motion{energy = Energy, time = Time}) ->
    [NewEnergy, NewTime] = calc_energy_2(Energy, Time),
    SceneMotion#scene_motion{energy = NewEnergy, time = NewTime};
calc_energy(RoleMotion = #role_motion{energy = Energy, time = Time}) ->
    [NewEnergy, NewTime] = calc_energy_2(Energy, Time),
    RoleMotion#role_motion{energy = NewEnergy, time = NewTime}.

%% 按变化时间重新计算能量
calc_energy_2(Energy, Time) ->
    NowTime = utime:unixtime(ms),
    if
        Energy =:= ?MOTION_ENERGY_MAX ->
            [Energy, NowTime];
        Time =:= 0 ->
            [Energy, NowTime];
        true ->
            EnergyAdd = max(NowTime - Time, 0) div ?MOTION_ENERGY_RECOVER_TIME,
            NewEnergy = min(Energy + EnergyAdd, ?MOTION_ENERGY_MAX),
            [NewEnergy, NowTime]
    end.

-spec login_init() -> RoleMotion :: #role_motion{}.
%% 登录初始化数据
%% 鉴于自然恢复到能量上限值所需时间最长才几秒，
%% 所以不做数据库存储，如果玩家重新登录，则恢复为满值
login_init() ->
    #role_motion{energy = ?MOTION_ENERGY_MAX}.

-spec to_scene_motion(RoleMotion :: #role_motion{}) -> SceneMotion :: #scene_motion{}.
%% 进入场景时，角色role_motion转换场景scene_motion
to_scene_motion(#role_motion{energy = Energy, time = Time}) ->
    [NewEnergy, _] = calc_energy_2(Energy, Time),
    #scene_motion{energy = NewEnergy}.

-spec check_cost_energy(Motion, MotionType) -> {NewMotion, FixMotionType} when
    Motion    :: #role_motion{} | #scene_motion{},
    NewMotion :: #role_motion{} | #scene_motion{},
    MotionType :: non_neg_integer(),
    FixMotionType :: non_neg_integer().
%% 运动能量消耗
%% 在触发移动行为操作时进行检查和修正
check_cost_energy(Motion, MotionType) ->
    EnergyCost =
    case MotionType of
        ?MOVE_ROLE_JUMP_COST_ENG ->
            ?MOTION_ENERGY_JUMP_COST;
        ?MOVE_ROLE_FLASH ->
            ?MOTION_ENERGY_FLASH_COST;
        _Other ->
            0
    end,
    do_cost_energy(Motion, MotionType, EnergyCost).

do_cost_energy(Motion, MotionType, 0) ->
    {Motion, MotionType};
do_cost_energy(Motion, MotionType, EnergyCost) ->
    MotionCalc = calc_energy(Motion),
    case MotionCalc of
        #scene_motion{energy = Energy, time = Time} when Energy > EnergyCost ->
            case MotionType of
                ?MOVE_ROLE_FLASH ->
                    %% calc_energy中一定会更新时间，time为NowTime
                    NoInjuryTime = Time + ?MOTION_FLASH_NO_INJURY,
                    NewEnergy = Energy - EnergyCost,
                    NewMotion = MotionCalc#scene_motion{
                        energy = NewEnergy
                        ,no_injury_time = NoInjuryTime
                    },
                    {NewMotion, MotionType};
                _ ->
                    NewEnergy = Energy - EnergyCost,
                    NewMotion = MotionCalc#scene_motion{energy = NewEnergy},
                    {NewMotion, MotionType}
            end;
        #role_motion{energy = Energy} when Energy > EnergyCost ->
            NewEnergy = Energy - EnergyCost,
            NewMotion = MotionCalc#role_motion{energy = NewEnergy},
            {NewMotion, MotionType};
        _ ->
            {MotionCalc, ?MOVE_ROLE_WALK}
    end.

%% 检测是否需要以异步方式将能量数据同步到角色进程（由场景进程调用）
%% 注意：
%%   MotionType应为场景进程接收到的请求参数，而不是修正后的参数，
%%   这样在存在可能的差异时，也会主动同步一次到角色进程，尽量保持一致
%% NOTE: _ClsType 用于判断是否跨服，决定是否做时间校正，暂时未使用
check_async_energy(Pid, SceneMotion, MotionType, _ClsType) ->
    case lists:member(MotionType, [?MOVE_ROLE_JUMP_COST_ENG, ?MOVE_ROLE_FLASH]) of
        true  ->
            #scene_motion{energy = Energy} = SceneMotion,
            gen_server:cast(Pid, {'set_data', [{motion_energy, Energy}]});
        false ->
            ignore
    end.

%% 同步角色运动能量
sync_motion_energy(Player = #player_status{sid = Sid, role_motion = RoleMotion}, NewEnergy) ->
    %% 节点中玩家收到消息时刻认为这是最新数据（忽略通讯延迟）
    NowTime = utime:unixtime(ms),
    NewRoleMotion = RoleMotion#role_motion{energy = NewEnergy, time = NowTime},
    %% 闪现会产生无敌效果，这里也不通知前端了，由前端收到移动消息时决定是否模拟表现
    %% 变化时通知前端能量值
    send_energy_msg(Sid, NewEnergy),
    Player#player_status{role_motion = NewRoleMotion};
sync_motion_energy(Player, _) ->
    Player.

