diff --git a/game-server/app/servers/activity/handler/gachaHandler.ts b/game-server/app/servers/activity/handler/gachaHandler.ts new file mode 100644 index 000000000..9d59905c1 --- /dev/null +++ b/game-server/app/servers/activity/handler/gachaHandler.ts @@ -0,0 +1,94 @@ +import { Application, BackendSession } from "pinus"; +import { resResult } from "../../../pubUtils/util"; +import { STATUS, GACHA_ID } from "../../../consts"; +import { gameData } from "../../../pubUtils/data"; +import { GachaListReturn } from "../../../domain/activityField/gachaField"; +import { UserGachaModel } from "../../../db/UserGacha"; +import { refreshFreeCount } from "../../../services/gachaService"; + + +export default function (app: Application) { + return new GachaHandler(app); +} + +export class GachaHandler { + constructor(private app: Application) { + } + + /** + * @description 获取抽卡列表 元宝招募、友情点招募、求贤若渴 + * @param {{}} msg + * @param {BackendSession} session + * @memberof GachaHandler + */ + async getGachaList(msg: {}, session: BackendSession) { + const { } = msg; + const roleId: string = session.get('roleId'); + + let userGachaList = await UserGachaModel.findAllByRole(roleId); + let list: GachaListReturn[] = []; + for(let [id, dicGacha] of gameData.gacha) { + if(id == GACHA_ID.TIMELIMIT) continue; // 不包括限时 + + let userGacha = userGachaList.find(cur => cur.gachaId == id); + userGacha = await refreshFreeCount(dicGacha, userGacha); + let param = new GachaListReturn(dicGacha, userGacha); + list.push(param); + } + + return resResult(STATUS.SUCCESS, { list }); + } + + /** + * @description 抽卡,所有抽卡类型都 + * @param {{ gachaId: number, activityId: number, count: number }} msg + * @param {BackendSession} session + * @memberof GachaHandler + */ + async pull(msg: { gachaId: number, activityId: number, count: number }, session: BackendSession) { + const { gachaId, activityId, count } = msg; + const roleId: string = session.get('roleId'); + + return resResult(STATUS.SUCCESS); + } + + /** + * @description 设置心愿单 + * @param {{ gachaId: number, hope: { id: number, hid: number }[] }} msg + * @param {BackendSession} session + * @memberof GachaHandler + */ + async setHope(msg: { gachaId: number, hope: { id: number, hid: number }[] }, session: BackendSession) { + const { gachaId, hope } = msg; + const roleId: string = session.get('roleId'); + + return resResult(STATUS.SUCCESS); + } + + /** + * @description 转盘 + * @param {{ gachaId: number }} msg + * @param {BackendSession} session + * @memberof GachaHandler + */ + async drawTurnTable(msg: { gachaId: number }, session: BackendSession) { + const { gachaId } = msg; + const roleId: string = session.get('roleId'); + + return resResult(STATUS.SUCCESS); + } + + + /** + * @description 求贤若渴&限时招募里面设置pick武将 + * @param {{ gachaId: number, pickHero: number }} msg + * @param {BackendSession} session + * @memberof GachaHandler + */ + async setPickHero(msg: { gachaId: number, pickHero: number }, session: BackendSession) { + const { gachaId, pickHero } = msg; + const roleId: string = session.get('roleId'); + + return resResult(STATUS.SUCCESS); + } +} \ No newline at end of file diff --git a/game-server/app/services/gachaService.ts b/game-server/app/services/gachaService.ts index a3b1338b6..69d54c55f 100644 --- a/game-server/app/services/gachaService.ts +++ b/game-server/app/services/gachaService.ts @@ -1,5 +1,10 @@ -import { GachaData } from "../domain/activityField/gachaField";; +import { GachaData, Floor } from "../domain/activityField/gachaField";; import { ActivityModelType, ActivityModel } from "../db/Activity"; +import { DicGacha } from "../pubUtils/dictionary/DicGacha"; +import { UserGachaType, UserGachaModel } from "../db/UserGacha"; +import { shouldRefresh } from "../pubUtils/util"; +import { REFRESH_HOUR, GACHA_ID, GACHA_TO_FLOOR } from "../consts"; +import { getNextDayByGap } from "../pubUtils/timeUtil"; /** * 获取活动页签里的限时卡池 @@ -27,4 +32,42 @@ export async function getLimitGacha(activityId: number) { count: 0 }] } +} + +/** + * 刷新免费次数 + * @param dicGacha + * @param userGacha + */ +export async function refreshFreeCount(dicGacha: DicGacha, userGacha: UserGachaType) { + let { day, count } = dicGacha.free; + if(count > 0) { + return userGacha; + } + + let { roleId, gachaId, refFreeTime } = userGacha; + if(shouldRefresh(refFreeTime, new Date(), REFRESH_HOUR, day)) { + let ref = getNextDayByGap(refFreeTime, new Date(), day); + userGacha = await UserGachaModel.refreshFreeCount(roleId, gachaId, 0, ref); + } + return userGacha +} + +/** + * @description 获取保底状态 + * @param type 招募类型 + * @param floor 玩家保底 + */ +export function getFloorStatus(type: number, floor: Floor[]) { + let floorMap = new Map(); + for(let { id, count } of floor) { + floorMap.set(id, count); + } + let dicFloorType = GACHA_TO_FLOOR.get(type); + return dicFloorType.map(id => { + return { + id, + count: floorMap.get(id)||0 + } + }); } \ No newline at end of file diff --git a/shared/consts/constModules/sysConst.ts b/shared/consts/constModules/sysConst.ts index db7f05473..249e16a3b 100644 --- a/shared/consts/constModules/sysConst.ts +++ b/shared/consts/constModules/sysConst.ts @@ -394,7 +394,6 @@ export const FILENAME = { DIC_FASHIONS: 'dic_zyz_fashions', DIC_FRIEND_SHIP: 'dic_zyz_friend_ship', DIC_FRIEND_SHIP_LEVEL: 'dic_zyz_friend_ship_level', - DIC_GACHA: 'dic_gacha', DIC_GK_BRANCH: 'dic_zyz_gk_branch', DIC_GK_DAILY: 'dic_zyz_gk_daily', DIC_GK_DUNGEON: 'dic_zyz_gk_dungeon', @@ -473,6 +472,8 @@ export const FILENAME = { DIC_MAIN_TASK_STAGE: 'dic_zyz_mainTaskStage', DIC_TASK_BOX: 'dic_zyz_taskBox', DIC_ACHIEVEMENT_BOX: 'dic_zyz_achievementBox', + DIC_GACHA: 'dic_zyz_gacha', + DIC_GACHA_CONTENT: 'dic_zyz_recruitContent', } export const WAR_RELATE_TABLES = [ @@ -652,4 +653,32 @@ export enum TASK_TYPE { GUILD_BOSS = 69, // 军团演武台挑战 GUILD_TRAIN = 70, // 挑战练兵场 GUILD_ACTIVITY = 71, // 军团活动 +} + +// 卡池类型 +export enum GACHA_ID { + NORMAL = 1, // 元宝招募 + FRDPOINT = 2, // 友情点 + ASSIGN = 3, // 指定卡池 + TIMELIMIT = 4, // 限时 +} + +// 抽卡保底类型 +export enum GACHA_FLOOR_TYPE { + PURPLE = 1, // 元宝招募,保底紫色 + GOLD = 2, // 元宝招募,保底金色 + ASSIGN = 3, // 求贤若渴,保底指定武将 +} + +// 抽卡对应保底类型 +export const GACHA_TO_FLOOR = new Map([ + [ GACHA_ID.NORMAL, [ GACHA_FLOOR_TYPE.PURPLE, GACHA_FLOOR_TYPE.GOLD ] ], + [ GACHA_ID.FRDPOINT, [] ], + [ GACHA_ID.ASSIGN, [ GACHA_FLOOR_TYPE.ASSIGN ] ], + [ GACHA_ID.TIMELIMIT, [ GACHA_FLOOR_TYPE.ASSIGN ] ] +]) + +// 抽卡里的卡池道具类型 +export enum GACHA_CONTENT_TYPE { + } \ No newline at end of file diff --git a/shared/db/UserGacha.ts b/shared/db/UserGacha.ts new file mode 100644 index 000000000..16eb035a8 --- /dev/null +++ b/shared/db/UserGacha.ts @@ -0,0 +1,62 @@ +import BaseModel from './BaseModel'; +import { index, getModelForClass, prop, DocumentType, modelOptions } from '@typegoose/typegoose'; +import { getCurWeekDate, getTodayZeroDate } from '../pubUtils/timeUtil'; +import { Floor, Hope, Turntable } from '../domain/activityField/gachaField'; +import { REFRESH_HOUR } from '../consts'; + +/** + * 玩家抽卡表 +**/ +@modelOptions({ schemaOptions: { id: false } }) +@index({ roleId: 1, itemId: 1 }) + +export default class UserGacha extends BaseModel { + + @prop({ required: true }) + roleId: string; // 玩家id + + @prop({ required: true }) + gachaId: number; // 抽卡id 1-元宝 2-友情 3-指定 4-限时 + + @prop({ required: true, default: 0 }) + aid: number; // 限时抽卡对应活动id + + @prop({ required: true, default: 0 }) + count: number; // 已抽卡次数 + + @prop({ required: true, type: (() => Floor)(), default: [] }) + floor: Floor[]; // 已抽卡次数 + + @prop({ required: true, default: 0 }) + freeCount: number; // 免费次数 + + @prop({ required: true, default: () => { return getTodayZeroDate(REFRESH_HOUR) } }) + refFreeTime: Date; // 免费次数刷新时间 + + @prop({ required: true, type: (() => Hope)(), default: [] }) + hope: Hope[]; // 心愿单 + + @prop({ required: true, default: 0 }) + point: number; // 积分 + + @prop({ required: true, type: (() => Turntable)(), default: [] }) + turntable: Turntable[]; // 转盘 + + @prop({ required: true, default: 0 }) + pickHero: number; // 玩家指定武将 + + public static async findAllByRole(roleId: string) { + let rec: UserGachaType[] = await UserGachaModel.find({ roleId }).lean(); + return rec; + } + + public static async refreshFreeCount(roleId: string, gachaId: number, aid: number, refFreeTime: Date) { + let rec: UserGachaType = await UserGachaModel.findOneAndUpdate({ roleId, gachaId, aid }, { $set: { freeCount: 0, refFreeTime } }, { new: true }).lean(); + return rec; + } +} + +export const UserGachaModel = getModelForClass(UserGacha); + +export interface UserGachaType extends Pick, keyof UserGacha> { } +export type UserGachaParam = Partial; \ No newline at end of file diff --git a/shared/domain/activityField/gachaField.ts b/shared/domain/activityField/gachaField.ts index cfd2005c8..f4eda0639 100644 --- a/shared/domain/activityField/gachaField.ts +++ b/shared/domain/activityField/gachaField.ts @@ -1,16 +1,17 @@ -import { prop } from '@typegoose/typegoose'; import { ActivityModelType } from '../../db/Activity'; import { ActivityBase } from './activityField'; +import { prop } from '@typegoose/typegoose'; +import { UserGachaType } from '../../db/UserGacha'; +import { getSeconds } from '../../pubUtils/timeUtil'; +import { DicGacha } from '../../pubUtils/dictionary/DicGacha'; +import { getFloorStatus } from '../../services/gachaService'; // 抽卡数据 export class GachaData extends ActivityBase { - @prop({ required: true }) gachaId: number = 0; - @prop({ required: true }) heroes: Array = []; - public initData(data: string) { let obj = JSON.parse(data); this.gachaId = obj.gachaId; @@ -20,4 +21,71 @@ export class GachaData extends ActivityBase { constructor(activityData: ActivityModelType) { super(activityData) } +} + +/** + * @description 保底记录 + * @memberof UserGacha + */ +export class Floor { + @prop({ required: true }) + id: number; // 保底类型 1-紫将保底 2-金将保底 3-指定将保底 + @prop({ required: true }) + count: number; // 抽卡次数 +} + +/** + * @description 心愿单 + * @memberof UserGacha + */ +export class Hope { + @prop({ required: true }) + id: number; // 位置 + @prop({ required: true }) + hid: number; // 武将id + @prop({ required: true }) + hasGet: boolean; // 是否得到 +} + +/** + * @description 转盘记录 + * @memberof UserGacha + */ +export class Turntable { + @prop({ required: true }) + quality: number; // 品质 + @prop({ required: true }) + hasGet: boolean; // 是否得到 +} + +/** + * @description 获取抽卡界面的返回参数 + */ +export class GachaListReturn { + gachaId: number; // dicGacha里的id + freeCount: number = 0; // 免费次数 + refFreeTime: number = 0; // 免费次数下次刷新时间 + count: number = 0; // 整体已抽卡次数 + floor: Floor[] = []; // 整体保底 + hope: Hope[] = []; // 心愿单 + point: number = 0; // 积分 + turntable: Turntable[]; // 转盘记录 + pickHero: number = 0; // 玩家选择的武将 + + constructor(dicGacha: DicGacha, userGacha: UserGachaType) { + this.gachaId = dicGacha.id; + + if(userGacha) { + this.freeCount = dicGacha.free.count - userGacha.freeCount; + this.refFreeTime = getSeconds(userGacha.refFreeTime); + this.count = userGacha.count; + this.floor = getFloorStatus(dicGacha.id, userGacha.floor); + this.hope = userGacha.hope; + this.point = userGacha.point; + this.turntable = userGacha.turntable; + this.pickHero = userGacha.pickHero; + } else { + this.floor = getFloorStatus(dicGacha.id, []); + } + } } \ No newline at end of file diff --git a/shared/pubUtils/data.ts b/shared/pubUtils/data.ts index a133c6059..e061555a1 100644 --- a/shared/pubUtils/data.ts +++ b/shared/pubUtils/data.ts @@ -76,6 +76,8 @@ import { dicRankReward } from "./dictionary/DicRankReward"; import { dicTaskType, taskMap, dicMainTask, dicDailyTask, dicAchievement } from "./dictionary/DicTask"; import { dicMainTaskStage } from "./dictionary/DicMainTaskStage"; import { dicTaskBox } from './dictionary/DicTaskBox'; +import { dicGacha } from "./dictionary/DicGacha"; +import { dicGachaContent } from "./dictionary/DicGachaContent"; export const gameData = { blurprtCompose: dicBlueprtCompose, @@ -181,7 +183,9 @@ export const gameData = { dailyTask: dicDailyTask, achievement: dicAchievement, mainTaskStage: dicMainTaskStage, - taskBox: dicTaskBox + taskBox: dicTaskBox, + gacha: dicGacha, + gachaContent: dicGachaContent, }; // 在此提供一些原先在gamedata中提供的方法,以便更方便获取gameData数据 diff --git a/shared/pubUtils/dictionary/DicGacha.ts b/shared/pubUtils/dictionary/DicGacha.ts new file mode 100644 index 000000000..d80b20cc1 --- /dev/null +++ b/shared/pubUtils/dictionary/DicGacha.ts @@ -0,0 +1,54 @@ +import { readJsonFile, parseNumberList, parseGoodStr, decodeArrayListStr } from '../util' +import { FILENAME } from '../../consts' +import { RewardInter } from '../interface'; + +export interface DicGacha { + // 抽卡id + readonly id: number; + // 抽卡次数 1次,10次 + readonly count: number[]; + // 免费次数 + readonly free: { day: number, count: number }; + // 消耗的招募券 + readonly cost: RewardInter[]; + // 概率 + readonly percent: { id: number, percent: number }[]; +} + +const str = readJsonFile(FILENAME.DIC_GACHA); +let arr = JSON.parse(str); + +export const dicGacha = new Map(); +arr.forEach(o => { + o.count = parseNumberList(o.count); + o.free = parseFree(o.free); + o.cost = parseGoodStr(o.cost); + o.percent = parsePercent(o.percent); + dicGacha.set(o.id, o); +}); + + +function parseFree(str: string) { + let arr = parseNumberList(str); + if(arr.length > 0 && arr.length != 2) { + throw new Error('dic_gacha free format err'); + } + if(arr.length == 0) { + return { day: 0, count: 0 } + } else { + return { day: arr[0], count: arr[1] } + } +} + +function parsePercent(str: string) { + let result = new Array<{ id: number, percent: number }>(); + if (!str) return result; + let decodeArr = decodeArrayListStr(str); + for (let [id, percent] of decodeArr) { + if (isNaN(parseInt(id)) || isNaN(parseInt(percent))) { + throw new Error('data table format wrong'); + } + result.push({ id: parseInt(id), percent: parseInt(percent) }); + } + return result +} \ No newline at end of file diff --git a/shared/pubUtils/dictionary/DicGachaContent.ts b/shared/pubUtils/dictionary/DicGachaContent.ts new file mode 100644 index 000000000..4019d8be1 --- /dev/null +++ b/shared/pubUtils/dictionary/DicGachaContent.ts @@ -0,0 +1,23 @@ +import { readJsonFile, parseNumberList } from '../util' +import { FILENAME } from '../../consts' + +export interface DicGachaContent { + // 内容id + readonly id: number; + // 道具类型 见sysConst 中的 GACHA_CONTENT_TYPE + readonly type: number; + // 对应参数 品质,等级,物品id 等 + readonly param: number[]; + // 道具数量 + readonly count: number; +} + +const str = readJsonFile(FILENAME.DIC_GACHA_CONTENT); +let arr = JSON.parse(str); + +export const dicGachaContent = new Map(); +arr.forEach(o => { + o.param = parseNumberList(o.param); + dicGachaContent.set(o.id, o); +}); + diff --git a/shared/pubUtils/timeUtil.ts b/shared/pubUtils/timeUtil.ts index 6bea91419..d6d04e2ea 100644 --- a/shared/pubUtils/timeUtil.ts +++ b/shared/pubUtils/timeUtil.ts @@ -1,4 +1,4 @@ -import { TIME_FORMAT, REFRESH_TIME } from '../consts'; +import { TIME_FORMAT, REFRESH_TIME, REFRESH_HOUR } from '../consts'; const PER_SECOND = 1 * 1000; const PER_DAY = 24 * 60 * 60; @@ -137,8 +137,22 @@ export function getNextHourPoint(hour: number) { return time; } +/** + * 返回今天的某个时间点 + * @param time + * @param hour + */ export function getTodayZeroDate(hour = 0) { - var date = new Date(); + return getZeroDate(new Date(), hour); +} + +/** + * 根据给定的时间获得某个时间点 + * @param time + * @param hour + */ +export function getZeroDate(time: Date, hour = 0) { + let date = new Date(time.getTime()); date.setHours(hour); date.setMinutes(0); date.setSeconds(0); @@ -226,4 +240,38 @@ export function getCurDay(needHandle = false, time?: Date) { export function setWeek(d?: number) { week = d; if(d == 7) week = 0; +} + +/** + * + * @param preDate + * @param curDate + */ +export function getDayGap(preDate: Date, curDate: Date) { + let cur = getZeroDate(curDate, REFRESH_HOUR); + let pre = getZeroDate(preDate, REFRESH_HOUR); + + return Math.floor((cur.getTime() - pre.getTime()) / PER_DAY) +} + +/** + * 获取某一天的 n * day 天之后 + * @param preDate + * @param curDate + * @param day + */ +export function getNextDayByGap(preDate: Date, curDate: Date, day: number) { + let gap = getDayGap(preDate, curDate); + let n = Math.ceil(gap / day); + let time = getAfterDateByDay(preDate, n); + return new Date(time); +} + +/** + * 获取某个时间的 day 天之后 + * @param day + */ +export function getAfterDateByDay(preDate: Date, day: number) { + let time = getZeroDate(preDate, REFRESH_HOUR).getTime() - day * PER_DAY; + return time; } \ No newline at end of file