前言

忙忙碌碌地又一周过去了,又到了每周博客的时间了,这一周疲惫地推各种功能,没带脑子思考,又没心思用半天时间做一个新的功能探讨,终了翻了翻以前设计的功能,接下来就拿自己设计的任务系统做讲解了。

需求

任务系统需求有三种:

  • 每日任务:每个任务有独立的奖励,每日还有活跃度,活跃度可以兑换奖励,任务有次数限制,每日凌晨3点刷新,并清除活跃度。
  • 周任务:周任务没有活跃度,除了个人奖励外,会增加公会积分,不可重复完成,每周一凌晨3点刷新。
  • 成就:主要奖励成就点(用于显示玩家水平的一种方式),成就不可刷新。

任务类型分类:

  • 聊天:世界频道发送**条信息 (日常)
  • 战斗:扫荡**关卡次 (日常)
  • 好友:添加**个好友 (日常)
  • 战斗:进行世界Boss达到**名 (周常、成就)
  • 登录:玩家累计登录**次数可完成 (日常、成就、周常)
  • 卡牌:玩家培养**次卡牌 (日常)
  • 卡牌:玩家拥有**张卡牌 (成就)
  • 卡牌:玩家拥有**张X星卡牌 (成就)

…我又不是策划就不一一枚举了

功能提取

  • 刷新处理
  • 任务类型分类
  • 任务分发
  • 任务记录
  • 任务触发
  • 奖励发送
  • 任务动态增加

…仔细想想提取的又不是那个的有条理

设计实现

终于可以晒代码,水篇幅了,作为一个服务器端程序员,讲故事的确不是我所长。

刷新

1、玩家每次登录时,上次刷新时间戳和当前时间做比较

function isEnableRefresh(lastTime) {
  let hour5Time = Math.floor(new Date().setHours(3, 0, 0, 0) / 1000);
  return ((lastTime < hour5Time) && ((lastTime < hour5Time - 86400) || (utils.timestamp() >= hour5Time)));
};

2、系统每日3天发送刷新事件,每个在线玩家的任务刷新

function sysDailyRefresh() {
  let nextTime = Math.floor(new Date().setHours(3, 0, 0, 0) / 1000) + 86400;
  let seconds = nextTime - utils.timestamp();
  setTimeout(() => {
    userDailyRefresh();
    sysDailyRefresh();
  }, seconds * 1000);
};

3.周任务,需要判断判断再添加今日是否是周一即可

任务类型分类

仔细分各种任务类型,再一一枚举

const TASK_MODE = {
    FINISH_TASK: {
      ALL:            11,  //所有
    },
    RANDOM_CARD: {
      NUM:            21,  //召唤-累计召唤
    },
    BATTLE: {
      MAIN:           31,  //战斗-主线剧情
      UNLIMITED:      32,  //战斗-无限模式
      WORLD_BOSS:     33,  //战斗-世界Boss
    },
    FRIEND: {
      HAVE:           41,  //好友-拥有好友
    },
    CARD: {
      LEVEL_DEV:      51,  //卡牌-培养
      LEVEL:          52,  //卡牌-等级
      STAR:           53,  //卡牌-星级拥有
    },
}

再具体细分抽象可以为三类

  • 累计型:需要玩家手动触发的,每次响应累计+1,比如累计登陆、聊天次数
  • 拥有型:总共拥有多少,比如拥有卡牌数量
  • 排名型:达到多少名,因为和累计判断相反,故单独抽出来
  • ALL型:完成所有任务,这种一般针对日常任务

任务分发

简单的采用玩家上线自动领取,毕竟是个坑钱的手游,那么任务触发的设计情怀。

任务记录

因为都是一些简单的任务,可以抽象成下面几个属性记录即可

{
    "type" : 0 //日常、周常、成就
    "mode" : 0 //任务具体枚举类型
    "cfgId" : 0 //对应静态配置ID,用于查询具体
    "num" : 0 //任务需求数量、名次、等级
}

刷新、完成领取奖励后,直接删除即可。 任务完成的状态只有玩家领取任务奖励成功才出发。

任务触发

分为条件触发和登录触发两种方式

登录触发

顾名思义,玩家登录后,需要遍历玩家数据,后直接触发任务,主要应对拥有型任务。 eg:每次玩家上线,都要计算玩家卡组数量,并触发任务发送任务类型、以及卡牌数量。

条件触发

直接举例说明,每次玩家获得卡牌都要计算玩家卡组数量,并触发任务发送任务类型、以及卡牌数量。

其实条件触发基本上所有任务类型都可以截获,但登录触发如果没有的话手动修改数据、新增成就任务等都无法兼容,所以登录触发是有必要的,尤其是针对一次性的成就。

下面是具体针四种抽象的任务类型的响应

/**
 * @description 常规通用任务触发
 * 累计型
 * @param {Object} objTask 任务Obj
 * @param {Number} num 数量
 * @return {Boolean} 是否移除任务监听
 */
function commonTrigger (objTask, num) {
  let isRemoveListener;
  do {
    isRemoveListener = false;
    let tbObjTask = Config.getTbObjTask(objTask.getAttrInt('type'), objTask.getAttrInt('cfgId'));
    if(!tbObjTask) {
      break;
    }
    let oldNum = objTask.getAttrInt('num');
    let maxNum = tbObjTask["num"];
    let isMax = (oldNum + num) >= maxNum;
    let newNum = isMax ? maxNum : (oldNum + num);
    objTask.setAttrInt('num', newNum);
    isRemoveListener = isMax;
  } while(false);
  return isRemoveListener;
}

/**
 * @description 常规通用任务触发
 * 拥有型
 * @param {Object} objTask 任务Obj
 * @param {Number} num 数量
 * @return {Boolean} 是否移除任务监听
 */
function commonHaveTrigger (objTask, num) {
  let isRemoveListener;
  do {
    let tbObjTask = Config.getTbObjTask(objTask.getAttrInt('type'), objTask.getAttrInt('cfgId'));
    if(!tbObjTask) {
      break;
    }
    let maxNum = tbObjTask["num"];
    let isMax = num >= maxNum;
    let newNum = isMax ? maxNum : num;
    objTask.setAttrInt('num', newNum);
    isRemoveListener = isMax;
  } while(false);
  return isRemoveListener;
}

/**
 * @description 常规通用任务触发
 * 排名型
 * @param {Object} objTask 任务Obj
 * @param {Number} num 名次
 * @return {Boolean} 是否移除任务监听
 */
function commonRankTrigger (objTask, num) {
  let isRemoveListener;
  do {
    let tbObjTask = Config.getTbObjTask(objTask.getAttrInt('type'), objTask.getAttrInt('cfgId'));
    if(!tbObjTask) {
      break;
    }
    let maxNum = tbObjTask["num"];
    let isMax = num <= maxNum;
    let newNum = isMax ? maxNum : num;
    objTask.setAttrInt('num', newNum);
    isRemoveListener = isMax;
  } while(false);
  return isRemoveListener;
}

/**
 * @description 常规通用任务触发
 * ALL型
 * !此类型有外部任务种类统计任务剩余数量为0时会出发一次
 */
function commonAll(objTask, type) {
  let isRemoveListener;
  do {
    isRemoveListener = false;
    if(objTask.getAttrInt('type') !== type) { //类型相同则完成
      break;
    }
    let tbObjTask = Config.getTbObjTask(objTask.getAttrInt('type'), objTask.getAttrInt('cfgId'));
    if(!tbObjTask) {
      break;
    }
    let maxNum = tbObjTask["num"];
    objTask.setAttrInt(maxNum);
    isRemoveListener = true;
  } while(false);
  return isRemoveListener;
};

小结

时间准备的不够充足,先写到这里吧,下篇再具体讲解。