diff --git a/game-server/app/servers/battle/handler/ladderHandler.ts b/game-server/app/servers/battle/handler/ladderHandler.ts new file mode 100644 index 000000000..bb47b5c76 --- /dev/null +++ b/game-server/app/servers/battle/handler/ladderHandler.ts @@ -0,0 +1,91 @@ + +import { Application, BackendSession, pinus, HandlerService, } from 'pinus'; +import { uniq, findWhere, findIndex, pick } from 'underscore'; +import { gameData, getPvpBoxBySeasonNumAndIndex } from '../../../pubUtils/data'; +import { RoleModel, RoleType } from '../../../db/Role'; +import { STATUS } from '../../../consts/statusCode'; +import { resResult, genCode, checkRoleIsRobot, robotIdComBack } from '../../../pubUtils/util'; +import { LadderMatchModel, LadderUpdateInter } from '../../../db/LadderMatch'; +import { getBuyCntCost, getLadderData, refreshLadderDaily } from '../../../services/ladderService'; +import { LadderDataReturn, LadderDefense, LadderDefenseHero } from '../../../domain/battleField/ladder'; +import { LadderMatchRecModel } from '../../../db/LadderMatchRec'; +import { HeroModel, HeroType } from '../../../db/Hero'; +import { LADDER } from '../../../pubUtils/dicParam'; +import { handleCost } from '../../../services/role/rewardService'; +import { ITEM_CHANGE_REASON } from '../../../consts'; + +export default function (app: Application) { + new HandlerService(app, {}); + return new LadderHandler(app); +} + +export class LadderHandler { + constructor(private app: Application) { + } + + //1获取主界面 + async getData(msg: {}, session: BackendSession) { + let roleId = session.get('roleId'); + + let result = await getLadderData(roleId, true) + return resResult(STATUS.SUCCESS, result); + } + + // 8. 设置防守阵容 + async saveDefense(msg: { warId: number, heroes: { actorId: number, ai: number, dataId: number, order: number }[] }, session: BackendSession) { + let { warId, heroes } = msg; + let roleId = session.get('roleId'); + + let ladderData = await LadderMatchModel.findByRoleId(roleId); + if(!ladderData) return resResult(STATUS.LADDER_NOT_OPEN); + + let hids: number[] = heroes.map(cur => cur.actorId); + let dbHeroes = await HeroModel.findByHidRange(hids, roleId, '_id hid ce', true); + + let defenseHeroes = heroes.map(cur => { + let dbHero = dbHeroes.find(ccur => ccur.hid == cur.actorId); + return dbHero && new LadderDefenseHero(cur, dbHero._id, dbHero.ce); + }).filter(cur => cur); + let defense = new LadderDefense(defenseHeroes, warId); + ladderData = await LadderMatchModel.updateByRoleId(roleId, { defense }); + // 返回 + let result = new LadderDataReturn(); + result.setLadderData(ladderData); + + return resResult(STATUS.SUCCESS, pick(result, 'defense')); + } + + + // 11. 购买次数 + async buyCnt(msg: { count: number }, session: BackendSession) { + let { count } = msg; + let roleId = session.get('roleId'); + let sid = session.get('sid'); + + let ladderData = await LadderMatchModel.findByRoleId(roleId); + if(!ladderData) return resResult(STATUS.LADDER_NOT_OPEN); + + let update: LadderUpdateInter = {}; + // 刷新次数 + let refOppObj = refreshLadderDaily(ladderData); + if(refOppObj.shouldRefOpp) { + update = { ...refOppObj }; + } + + if(refOppObj.buyCnt + count > LADDER.LADDER_CHALLENGE_COST_TIMES) { + return resResult(STATUS.LADDER_BUY_MAX); + } + let consumes = getBuyCntCost(refOppObj.buyCnt, count); + if(!consumes) return resResult(STATUS.LADDER_BUY_MAX); + + let consumeResult = await handleCost(roleId, sid, consumes||[], ITEM_CHANGE_REASON.LADDER_BUY_CNT); + if(!consumeResult) return resResult(STATUS.ROLE_MATERIAL_NOT_ENOUGH); + + update.buyCnt = refOppObj.buyCnt + count; + ladderData = await LadderMatchModel.updateByRoleId(roleId, update); + // 返回 + let result = new LadderDataReturn(); + result.setLadderData(ladderData); + return resResult(STATUS.SUCCESS, pick(result, 'buyCnt', 'challengeCnt')); + } +} \ No newline at end of file diff --git a/game-server/app/services/connectorService.ts b/game-server/app/services/connectorService.ts index ed898bea8..0f7ae70a6 100644 --- a/game-server/app/services/connectorService.ts +++ b/game-server/app/services/connectorService.ts @@ -46,6 +46,7 @@ import { sendMessageToAllWithSuc, sendMessageToServerWithSuc, sendMessageToUser, import { getSurvey } from './gmService'; import { ComBattleTeamModel } from '../db/ComBattleTeam'; import { INFO_WINDOW } from '../pubUtils/dicParam'; +import { getLadderData } from './ladderService'; /** * init: 初始的时候是否推送 true-推 false-不推 @@ -79,6 +80,7 @@ const modules = [ { id: 24, type: 'donate', init: false, refresh: true, guild: true }, { id: 25, type: 'shop', init: false, refresh: true, guild: false }, { id: 26, type: 'survey', init: false, refresh: true, guild: false }, + { id: 27, type: 'ladder', init: false, refresh: true, guild: false }, ] export async function pushData(hasInit: boolean, role: RoleType, session: FrontendOrBackendSession, pushType: 'entry' | 'refresh' = 'entry') { @@ -206,6 +208,8 @@ export async function getModuleData(type: string, data: { role: RoleType, sessio return getMainChapter(role); case 'survey': return await getSurvey(role.roleId, role.lv); + case 'ladder': + return await getLadderData(role.roleId, false); default: return null; } diff --git a/game-server/app/services/ladderService.ts b/game-server/app/services/ladderService.ts new file mode 100644 index 000000000..c7b49a66a --- /dev/null +++ b/game-server/app/services/ladderService.ts @@ -0,0 +1,66 @@ +import { LadderMatchModel, LadderMatchType } from "../db/LadderMatch"; +import { LadderMatchRecModel } from "../db/LadderMatchRec"; +import { RoleModel } from "../db/Role"; +import { LadderDataReturn } from "../domain/battleField/ladder"; +import { gameData } from "../pubUtils/data"; +import { ItemInter } from "../pubUtils/interface"; +import { shouldRefresh } from "../pubUtils/util"; +import { combineItems } from "./role/util"; + +export function refreshLadderDaily(ladderData: LadderMatchType) { + + let { refOppCnt = 0, buyCnt = 0, challengeCnt = 0, refDaily } = ladderData; + let curTime = new Date(); + let shouldRefOpp = shouldRefresh(refDaily, curTime); + if (shouldRefOpp) { + refOppCnt = 0; buyCnt = 0; challengeCnt =0; refDaily = curTime; + } + return { + shouldRefOpp, + refOppCnt, refDaily, buyCnt, challengeCnt, + } +} + +export function getBuyCntCost(buyCnt: number, incCnt: number) { + let items: ItemInter[] = []; + for(let i = 1; i <= incCnt; i++) { + let cost = getSingleBuyCost(buyCnt + i); + if(!cost) return false; + items.push(cost); + } + return combineItems(items); +} + +function getSingleBuyCost(buyCnt: number) { + for(let { id, count, times } of gameData.ladderConsume) { + if(buyCnt <= times) { + return { id, count } + } + } + return false +} + +export async function getLadderData(roleId: string, needOppPlayer: boolean) { + let ladderData = await LadderMatchModel.findByRoleId(roleId); + if(!ladderData) { + let role = await RoleModel.findByRoleId(roleId); + ladderData = await LadderMatchModel.createLadder({ serverId: role.serverId, roleId: role.roleId, role: role._id }); + } + + // 刷新次数 + let refOppObj = refreshLadderDaily(ladderData); + if(refOppObj.shouldRefOpp) { + ladderData = await LadderMatchModel.updateByRoleId(roleId, refOppObj); + } + + let ladderRec = await LadderMatchRecModel.findNotCompleteRecByRoleId(roleId); + + let result = new LadderDataReturn(); + result.setLadderData(ladderData, ladderRec); + + if(needOppPlayer) { + // let oppPlayersReturn = await getEnemies(ladderData.oppPlayers||[], ladderData.winStreakNum); + // result.setOppPlayers(oppPlayersReturn); + } + return result; +} \ No newline at end of file diff --git a/shared/consts/constModules/battleConst.ts b/shared/consts/constModules/battleConst.ts index f572238e3..ac7ac8b23 100644 --- a/shared/consts/constModules/battleConst.ts +++ b/shared/consts/constModules/battleConst.ts @@ -156,4 +156,13 @@ export enum TOWER_FORBIDDEN_CHARA_TYPE { CHAR = 1, // 角色 JOB = 2, // 职业 CAMP = 3, // 国家 -} \ No newline at end of file +} + +export enum LADDER_STATUS { + NO = 0, // 无战场 + CHECK = 1, // 出兵中 + BATTLE = 2, // 战斗中 +} + +export const LADDER_CHECK_STOP_TIME = 2 * 60; +export const LADDER_BATTLE_STOP_TIME = 5 * 60; \ No newline at end of file diff --git a/shared/consts/constModules/sysConst.ts b/shared/consts/constModules/sysConst.ts index 84335ad9d..38ae97c59 100644 --- a/shared/consts/constModules/sysConst.ts +++ b/shared/consts/constModules/sysConst.ts @@ -1012,6 +1012,7 @@ export enum ITEM_CHANGE_REASON { RECEIVE_TOWER_BOX = 148, // 领取镇念塔节点奖励 ACT_DAILY_COIN_RECEIVE_BOX = 149, // 招财进宝领取宝箱 USE_VOUCHER = 150, // 使用代金券 + LADDER_BUY_CNT = 151, // 名将擂台购买次数 } export enum TA_EVENT { diff --git a/shared/consts/statusCode.ts b/shared/consts/statusCode.ts index 52d3c8e0a..80e44c2c1 100644 --- a/shared/consts/statusCode.ts +++ b/shared/consts/statusCode.ts @@ -258,6 +258,10 @@ export const STATUS = { CAN_NOT_DECLARE: { code: 21109, simStr: '该城池不可宣战' }, CHALLENGE_TIME_NOT_NEED_RESET: { code: 21110, simStr: '不需要重置' }, + // 名将擂台 + LADDER_NOT_OPEN: { code: 21200, simStr: '名将擂台未开放' }, + LADDER_BUY_MAX: { code: 21201, simStr: '名将擂台购买次数达到上限' }, + // 通用 30000 - 30099 DIC_DATA_NOT_FOUND: { code: 30000, simStr: '数据表未找到' }, ROLE_MATERIAL_NOT_ENOUGH: { code: 30001, simStr: '材料数量不足' }, diff --git a/shared/db/LadderMatch.ts b/shared/db/LadderMatch.ts new file mode 100644 index 000000000..cbdd9b15f --- /dev/null +++ b/shared/db/LadderMatch.ts @@ -0,0 +1,54 @@ +import BaseModel from './BaseModel'; +import { index, getModelForClass, prop, DocumentType, mongoose, Ref } from '@typegoose/typegoose'; +import { LadderDefense, } from '../domain/battleField/ladder'; +import Role from './Role'; + +@index({ roleId: 1 }) +export default class LadderMatch extends BaseModel { + @prop({ required: true, default: 0 }) + serverId: number; // 角色 id + @prop({ required: true, default: '' }) + roleId: string; // 角色 id + @prop({ ref: 'Role', type: mongoose.Schema.Types.ObjectId }) + role: Ref; + @prop({ required: true, default: 0 }) + rank: number; // 排名 + @prop({ required: true, default: 0 }) + historyRank: number; // 历史最高排名 + + @prop({ required: true, default: null, _id: false }) + defense: LadderDefense; + + @prop({ required: true, default: () => { return new Date() } }) + refDaily: Date; // 每日刷新,控制refOppCnt和setAttackCnt + @prop({ required: true, default: 0 }) + challengeCnt: number; // 挑战次数 + @prop({ required: true, default: 0 }) + buyCnt: number; // 购买次数 + @prop({ required: true, default: 0 }) + refOppCnt: number; // 刷新对手次数 + + public static async findByRoleId(roleId: string, getters = false) { + const result: LadderMatchType = await LadderMatchModel.findOne({ roleId }).lean({ getters}); + return result; + } + + public static async createLadder(params: LadderUpdateInter) { + const doc = new LadderMatchModel(); + + const update = Object.assign(doc.toJSON(), params); + const defense: LadderMatchType = await LadderMatchModel.findOneAndUpdate({ roleId: params.roleId }, { $set: update}, { upsert: true, new: true }).lean(); + return defense; + + } + + public static async updateByRoleId(roleId: string, params: LadderUpdateInter) { + const defense: LadderMatchType = await LadderMatchModel.findOneAndUpdate({ roleId }, { $set: params}, { new: true }).lean(); + return defense; + } +} + +export const LadderMatchModel = getModelForClass(LadderMatch); + +export interface LadderMatchType extends Pick, keyof LadderMatch> { }; +export type LadderUpdateInter = Partial; \ No newline at end of file diff --git a/shared/db/LadderMatchRec.ts b/shared/db/LadderMatchRec.ts new file mode 100644 index 000000000..589874828 --- /dev/null +++ b/shared/db/LadderMatchRec.ts @@ -0,0 +1,50 @@ +import BaseModel from './BaseModel'; +import { index, getModelForClass, prop, DocumentType, Ref, mongoose } from '@typegoose/typegoose'; +import Role, { } from './Role'; +import { Defense, Attack, LineupCe, OppPlayer, HeroScore, } from '../domain/battleField/pvp'; +import { CounterModel } from './Counter'; +import { COUNTER, LADDER_STATUS } from '../consts'; +import { EXTERIOR, PVP } from '../pubUtils/dicParam'; +import { LadderDefense, LadderOppPlayerInfo } from '../domain/battleField/ladder'; + + +@index({ battleCode: 1 }) +export default class LadderMatchRec extends BaseModel { + @prop({ required: true }) + battleCode: string; // 唯一code + @prop({ required: true }) + roleId1: string; // 攻方玩家id + @prop({ required: true }) + roleId2: string; // 守方玩家id + @prop({ required: true }) + serverId: number; // 区 id + @prop({ required: true }) + status: LADDER_STATUS; // 1-出兵中 2-挑战中 3-挑战完毕 + @prop({ required: true }) + checkTime: number; // 开始出兵的时间,10位时间戳 + @prop({ required: true }) + battleTime: number; // 开始挑战的时间,10位时间 + @prop({ required: true }) + endTime: number; // 结束时间 + @prop({ required: true, type: LadderOppPlayerInfo, default: {}, _id: false }) + attackInfo: () => LadderOppPlayerInfo; // 攻方信息 + @prop({ required: true, type: LadderOppPlayerInfo, default: {}, _id: false }) + defenseInfo: () => LadderOppPlayerInfo; // 守方信息 + @prop({ required: true, type: LadderDefense, default: {}, _id: false }) + defense: () => LadderDefense; // 守方信息 + + public static async findByRoleId(roleId: string, getters = false) { + const result: LadderMatchRecType = await LadderMatchRecModel.findOne({ roleId1: roleId }).lean({ getters}); + return result; + } + + public static async findNotCompleteRecByRoleId(roleId: string, getters = false) { + const result: LadderMatchRecType = await LadderMatchRecModel.findOne({ roleId1: roleId, status: { $in: [ LADDER_STATUS.BATTLE, LADDER_STATUS.CHECK ] } }).lean({ getters}); + return result; + } +} + +export const LadderMatchRecModel = getModelForClass(LadderMatchRec); + +export interface LadderMatchRecType extends Pick, keyof LadderMatchRec> { }; +export type pvpUpdateInter = Partial; \ No newline at end of file diff --git a/shared/domain/battleField/ladder.ts b/shared/domain/battleField/ladder.ts new file mode 100644 index 000000000..e59c993bd --- /dev/null +++ b/shared/domain/battleField/ladder.ts @@ -0,0 +1,183 @@ +import { mongoose, prop, Ref } from "@typegoose/typegoose"; +import { LADDER_BATTLE_STOP_TIME, LADDER_CHECK_STOP_TIME, LADDER_STATUS } from "../../consts"; +import Hero from '../../db/Hero'; +import { LadderMatchType } from "../../db/LadderMatch"; +import { LadderMatchRecType } from "../../db/LadderMatchRec"; +import { RoleType } from "../../db/Role"; +import { EXTERIOR } from "../../pubUtils/dicParam"; + +// ladderMatch表 +export class LadderDefenseHero { + @prop({ required: true }) + actorId: number; // 武将id + @prop({ required: true }) + ai: number; // ai逻辑,1-进攻型 2-防守型 + @prop({ required: true }) + dataId: number; + @prop({ required: true }) + order: number; + @prop({ ref: 'Hero', type: mongoose.Schema.Types.ObjectId }) + hero: Ref; + @prop({ required: true }) + ce: number; + + constructor(param: { actorId: number, ai: number, dataId: number, order: number }, heroId: string, ce: number) { + this.actorId = param.actorId; + this.ai = param.ai; + this.dataId = param.dataId; + this.order = param.order; + this.hero = heroId; + this.ce = ce; + } +} + +export class LadderDefense { + + @prop({ required: true }) + warId: number; + @prop({ required: true, type: LadderDefenseHero, _id: false }) + heroes: LadderDefenseHero[]; + + constructor(heroes: LadderDefenseHero[], warId: number) { + this.heroes = heroes; + this.warId = warId; + } +} + + +// ladderMatchRec +export class LadderOppPlayerHeroInfo { + @prop({ required: true }) + hid: number; // 武将id + @prop({ required: true }) + skinId: number; // 皮肤id + @prop({ required: true }) + quality: number; // 皮肤id + @prop({ required: true }) + star: number; // 皮肤id + @prop({ required: true }) + colorStar: number; // 皮肤id + @prop({ required: true }) + lv: number; // 皮肤id +} + +export class LadderOppPlayerInfo { + @prop({ required: true }) + oldRank: number; // 原排名 + @prop({ required: true }) + newRank: number; // 交换后的排名 + @prop({ required: true }) + roleId: string; // 角色 id + @prop({ required: true }) + roleName: string; // 角色 名 + @prop({ required: true, default: 0 }) + lv: number; // 等级 + @prop({ required: true, default: EXTERIOR.EXTERIOR_FACE }) + head: number; // 头像 + @prop({ required: true, default: EXTERIOR.EXTERIOR_FACECASE }) + frame: number; // 相框 + @prop({ required: true, default: EXTERIOR.EXTERIOR_APPEARANCE }) + spine: number; // 形象 + @prop({ required: true, default: 0 }) + title: number; // 爵位 + @prop({ required: true, default: 0 }) + ce: number; // 最高五人战力 + @prop({ required: true, default: false }) + isSuccess: boolean; // 是否胜利 + @prop({ required: true, default: 0 }) + score: number; // 获得的总积分 + @prop({ required: true, default: [], type: LadderOppPlayerHeroInfo, _id: false }) + heroes: LadderOppPlayerHeroInfo[]; // 获得的总积分 + + constructor(role: RoleType, heroes: LadderOppPlayerHeroInfo[], defCe: number, isSuccess: boolean ) { + this.roleId = role.roleId; + this.roleName = role.roleName; + this.head = role.head; + this.frame = role.frame; + this.spine = role.spine; + this.title = role.title; + this.ce = defCe; + this.lv = role.lv; + this.isSuccess = isSuccess; + this.heroes = heroes; + } +} + +// 接口返回 +export class LadderDefenseHeroReturn { + actorId: number; // 武将id + ai: number; // ai逻辑,1-进攻型 2-防守型 + dataId: number; + order: number; + + constructor(defenseHero: LadderDefenseHero) { + this.actorId = defenseHero.actorId; + this.ai = defenseHero.ai; + this.dataId = defenseHero.dataId; + this.order = defenseHero.order; + } +} + +export class LadderDefenseReturn { + warId: number; + defCe: number = 0; + heroes: LadderDefenseHeroReturn[] = []; + + constructor(defense: LadderDefense) { + if(!defense) return; + this.warId = defense.warId; + for(let defenseHero of defense.heroes) { + this.defCe += defenseHero.ce; + this.heroes.push(new LadderDefenseHeroReturn(defenseHero)); + } + } +} + +export class LadderOppPlayerReturn { + rank: number; // 对手拍名 + roleId: string; // 对手玩家id + roleName: string; // 对手玩家名 + head: number; // 头像 + frame: number; // 相框 + spine: number; // 形象 + lv: number; // 对手玩家等级 + title: number; // 对手爵位 + defCe: number; // 对手防守阵容战力 + isRobot: boolean; // 是否是机器人 +} + +export class LadderDataReturn { + rank: number = 0; + historyRank: number = 0; + challengeCnt: number = 0; + buyCnt: number = 0; + refOppCnt: number = 0; + defense: LadderDefenseReturn = null; + status: LADDER_STATUS; + time: number = 0; + oppPlayers: LadderOppPlayerReturn[] = [] + + setLadderData(ladderData: LadderMatchType, ladderRec?: LadderMatchRecType) { + this.rank = ladderData.rank; + this.historyRank = ladderData.historyRank; + this.challengeCnt = ladderData.challengeCnt; + this.buyCnt = ladderData.buyCnt; + this.refOppCnt = ladderData.refOppCnt; + if(ladderData.defense) { + this.defense = new LadderDefenseReturn(ladderData.defense); + } + if(ladderRec) { + this.status = ladderRec.status; + if(this.status == LADDER_STATUS.BATTLE) { + this.time = ladderRec.battleTime + LADDER_BATTLE_STOP_TIME; + } else if (this.status == LADDER_STATUS.CHECK) { + this.time = ladderRec.checkTime + LADDER_CHECK_STOP_TIME; + } + } + } + + setOppPlayers(oppPlayers: LadderOppPlayerReturn[]) { + this.oppPlayers = oppPlayers; + } + +} \ No newline at end of file diff --git a/shared/pubUtils/data.ts b/shared/pubUtils/data.ts index 3f8b8c512..29cd4a5c7 100644 --- a/shared/pubUtils/data.ts +++ b/shared/pubUtils/data.ts @@ -269,6 +269,7 @@ export const gameData = { towerPvpSubAttr: dicTowerPvpSubAttr, sysOpenTime: dicSystemOpenTime, randomEffectPoolPlan: dicRandomEffectPoolPlan, + ladderConsume: new Array<{id: number, count: number, times: number}>(), }; // 在此提供一些原先在gamedata中提供的方法,以便更方便获取gameData数据 @@ -969,6 +970,17 @@ export function getEquipStrenthenAttr(equipId: number, lv: number) { return result } +export function decodeLadderBuyCost() { + gameData.ladderConsume = []; + let str = param.LADDER.LADDER_CHALLENGE_BUY_TIMES_COST; + let arrs = decodeArrayListStr(str); + let sumTimes = 0; + for (let [ id, count, times] of arrs) { + sumTimes += parseInt(times); + gameData.ladderConsume.push({ id: parseInt(id), count: parseInt(count), times: sumTimes }); + } +} + // 初始加载 function initDatas() { parseDicParam(); @@ -987,6 +999,7 @@ function parseDicParam() { parseComBtlLvRange(); getCeRatio(); loadAuctionTime(); + decodeLadderBuyCost(); } /**