标签:tcl rdd model war box uid class 初始化 lan
<1>很多人都玩过炸弹人或者泡泡堂,之前做了这个玩法,记录一下关键实现
<2>直接上Lua代码
1.维护每个格子上的物体 比如:BUFF类,宝箱,草箱,木箱,铁箱,炸弹,小怪...
用一个结构维护一个格子31*31=961,进入场景创建,离开释放掉
每个结构里面声明一张map缓存格子里面所有的物体 <物体类型,map<物体uid,true>>
2.计算炸弹爆炸所影响的格子(长宽不等的十字形)
炸弹有一个威力等级Force,计算规则为 Force*2+1 = 同一轴上所影响的格子
但是碰到铁箱子是无法穿透的,所有这个爆炸的十字形可能是长宽不等的
BombermanMgr = { } local _isInit = false local rowNum = 31 local colNum = 31 local cellSize = 2 -- -22 30 对应 -21.945 29.925 要么场景做偏移 要么起始X Y做偏移 -0.055 0.075 -- 场景做偏移 local startX = -31 local startY = -31 local function create_self_param(self) if _isInit == ture then return end -- 导入依赖的文件 -- 初始化 self:__init_self_param() end local function create(self) create_self_param(self) end -- 初始化成员 function BombermanMgr:__init_self_param() self.boxPool = nil --缓存961格子信息 BoxData self.dropPool = { } --缓存掉落物 entity self.memberPool = { } --玩家信息 PPTMemberData self.rankPool = { }--玩家信息 PPTMemberRankData self.rankMap = { }--玩家排名 PPTMemberRankData self.skillPool = nil --玩家技能信息 self.mainRoleId = 0 --排行数据 self.todayReqTime = -1 --上次拉取时间 self.yestodayReqTime = -1 --上次拉取时间 self.todayLst = { } --今天数据 self.yestodayLst = { }--昨天数据 self.todayMine = nil --自己今天 self.yestodayMine = nil --自己昨天 self.myRespawnRoleId = nil --出生点模型 self.followId = -1 --跟随id self.lastAlertTime = -1--上次弹出提示时间 self.bombEffectTimeMap = { }--格子爆炸特效上次释放时间 self.audioTimeMap = { }--音效上次播放时间 --self.combineLst = ListLong() --需要合并的对象 self.combineRoot = nil --合并根节点 self.combineNum = 0 --无需清理 self.infoHandler = nil self.rankRewardLst = nil end function BombermanMgr:getMainPlayerId() return self.mainRoleId end function BombermanMgr:getMaxIndex() return rowNum*colNum end --技能信息 --新实体协议下发 初始化技能 根据实体中的宠物id获取充能次数... function BombermanMgr:initSkill(petId) self.skillPool = { } local pptCfg = BombermanUtils:getPetConfigById(petId) if not pptCfg then Logger:logError("没有读取到PptPetConfig id ",petId) return end local boomData = PPTSkillData(PPTSkillType.boom,PPTBombForceIcon[1],BombermanConst.bombCooldown,pptCfg.bomCharge) self.skillPool[PPTSkillType.boom] = boomData local speedData = PPTSkillData(PPTSkillType.speed,BombermanConst.speedIcon,BombermanConst.speedCooldown,pptCfg.speedRune) self.skillPool[PPTSkillType.speed] = speedData local superData = PPTSkillData(PPTSkillType.super,BombermanConst.superIcon,BombermanConst.superCooldown,pptCfg.snvincibleRune) self.skillPool[PPTSkillType.super] = superData local hpData = PPTSkillData(PPTSkillType.hp,BombermanConst.hpIcon,BombermanConst.hpCooldown,pptCfg.life) self.skillPool[PPTSkillType.hp] = hpData end function BombermanMgr:getSkillLst() return self.skillPool end --计数数据更新 function BombermanMgr:updateSkill(skillType,count,force) local dt = self.skillPool[skillType] if dt then if force then dt:setForceLevel(force) end if count then dt:onCountChange(count) end end end function BombermanMgr:addSkillCount(skillType) local dt = self.skillPool[skillType] if dt then dt:addChargeNum(1) end end --配置静态盒子 --2次for循环 中间少了的固定格子写死 function BombermanMgr:initBox() self.boxPool = { } --把固定格子创建出来 local ignore = { } ignore[419] = true ignore[479] = true ignore[481] = true ignore[483] = true ignore[543] = true for i = 1,15 do local num = 31+2*i for k = 1,15 do local num2 = num+31*(k-1)*2 if ignore[num2] == nil then local data = BoxData(BoxType.metalBox,MathUtils:getUniqueID()) self.boxPool[num2] = data end end end --创建剩余的格子 默认为空格子 for i = 1,rowNum*colNum do if self.boxPool[i] == nil then local data = BoxData(BoxType.emptyBox,MathUtils:getUniqueID()) self.boxPool[i] = data end end end --隐射一个实体类型 local entityMap = { } entityMap[BoxType.bombBox] = LBoxHinderBombEntity local function getClass(type) local class = entityMap[type] if class == nil then class = LBoxHinderEntity end return class end --创建静态实体 function BombermanMgr:createBox(msg) local index = msg.gridId local id = msg.livingId local btype = msg.type local masterId = msg.playerId local modelId = BoxType2Model[btype] if btype == BoxType.bombBox then modelId = masterId<=0 and BombermanConst.bossBombModelId or modelId --音效:炸弹创建 if masterId*10 == self:getMainPlayerId() then self:playAudio(BombermanConst.bombCastAudioId, nil) end end local pos = self:getBoxCenterPos(index) --data local roleData = LRoleData(id, RoleType.BoxHinder, true) roleData:changeAttr(RoleAttr.SkinId,modelId) roleData:changeAttr(RoleAttr.Fight,index) roleData:changeAttr(RoleAttr.Exp,btype) roleData:changeAttr(RoleAttr.MasterId,masterId) --roleData:changeAttr(RoleAttr.MountId,msg.blood) --roleData:changeAttr(RoleAttr.MagicWeaponId,msg.maxBlood) roleData:changeAttr(RoleAttr.isActive,true) roleData:changeAttr(RoleAttr.InitPosition, pos) roleData:setFull() roleData:attach() --创建 if StaticEntityMgr:getRoleData(id) ~= nil then return end local class = getClass(btype) local staticEntity = class(id, RoleType.BoxHinder, roleData , nil) if staticEntity then if self.boxPool[index] then self.boxPool[index]:addType(btype,id) end StaticEntityMgr:addRoleData(roleData) StaticEntityMgr:addRole(staticEntity) staticEntity:create() end end --移除触发器实体 function BombermanMgr:removeBox(msg) local id = msg.livingId local index = msg.gridId local btype = msg.type if self.boxPool[index] then self.boxPool[index]:removeType(btype,id) end StaticEntityMgr:removeRole(id) StaticEntityMgr:removeRoleData(id) --移除的盒子需要有特效 则播放特效 self:playEffect(btype,index) end --有的盒子销毁需要播放特效 function BombermanMgr:playEffect(btype,idx) if BoxType2ExplodeEffect[btype] ~= nil then local pos = self:getBoxCenterPos(idx) EffectMgr:createEffect(BoxType2ExplodeEffect[btype], pos, nil) end --宝箱移除 播放音效 if btype == BoxType.treasureBox then self:playAudio(BombermanConst.openTreasureAudioId, nil) end end --协议更新盒子 function BombermanMgr:updateBox(msg) if self.boxPool == nil then self:initBox() end local index = msg.gridId local btype = msg.type local uid = msg.livingId local isAdd = msg.blood > 0 --不再使用状态字段 blood就可以标记这个状态了 if self.boxPool[index] then if isAdd then self:createBox(msg) self.boxPool[index]:addType(btype,uid) else self:removeBox(msg) self.boxPool[index]:removeType(btype,uid) end end end --怪物移动 --在格子上添加移除 function BombermanMgr:simpleAddType(index,btype,uid) local boxData = self:getBoxData(index) if boxData then boxData:addType(btype,uid) end end function BombermanMgr:simpleRemoveType(index,btype,uid) local boxData = self:getBoxData(index) if boxData then boxData:removeType(btype,uid) end end --是否可以移动到目标位置 function BombermanMgr:canMoveTo(nowIndex,targetPos) local index = self:getCellIndex(targetPos) --如果玩家在当前格子 可以移动 if index == nowIndex then return true end --没有找到格子 无法移动 if self.boxPool[index] == nil then return true end --最后返回目标格子没有阻挡物 return not self.boxPool[index]:getIsHinder() end function BombermanMgr:canMoveToByPos(nowPos,targetPos) local index = self:getCellIndex(nowPos) return self:canMoveTo(index,targetPos) end --炸弹爆炸所有格子 --服务器推 爆炸点 炸弹等级 客户端计算出在哪些格子实例化炸弹 function BombermanMgr:getExplodeIndexs(index,level) local lst = { } local dirMap = { }--4个方向的数量 dirMap.top = 0 dirMap.bottom = 0 dirMap.left = 0 dirMap.right = 0 lst[1] = index local row = self:getIndexRow(index) --local col = self:getIndexCol(index) local boxData = self:getBoxData(index) if boxData == nil then return lst end local unlockTop = true local unlockBottom = true local unlockLeft = true local unlockRight = true for i = 1,level do local top = index-rowNum*i local bottom = index+rowNum*i local left = index-i local right = index+i --验证是否合理 --top不能<=0 bottom不能>31 left right 验证同一行既可 --公共条件 有的(铁格子)无需放炸弹 被铁格子挡住了 也无法继续延伸释放炸弹 if unlockTop and top > 0 then unlockTop,canAdd = self:canPlaceExplode(top,level) if unlockTop then dirMap.top = dirMap.top + 1 end if canAdd then table.insert(lst,top) end end if unlockBottom and bottom <= rowNum*colNum then unlockBottom,canAdd = self:canPlaceExplode(bottom,level) if unlockBottom then dirMap.bottom = dirMap.bottom + 1 end if canAdd then table.insert(lst,bottom) end end --left right if unlockLeft and self:getIndexRow(left) == row then unlockLeft,canAdd = self:canPlaceExplode(left,level) if unlockLeft then dirMap.left = dirMap.left + 1 end if canAdd then table.insert(lst,left) end end if unlockRight and self:getIndexRow(right) == row then unlockRight,canAdd = self:canPlaceExplode(right,level) if unlockRight then dirMap.right = dirMap.right + 1 end if canAdd then table.insert(lst,right) end end end return lst,dirMap end --格子是否可以放置炸弹爆炸物 --return 1是否被阻挡 2是否可以爆炸 function BombermanMgr:canPlaceExplode(index,level) local boxData = self:getBoxData(index) if boxData == nil then return true,true end if boxData:hasType(BoxType.metalBox) then return false,false elseif boxData:hasType(BoxType.grassBox) then return false,true--level>=3 elseif boxData:hasType(BoxType.woodBox) then return false,true--level>=1 end return true,true --return not boxData:hasType(BoxType.metalBox) and not boxData:hasType(BoxType.grassBox) and not boxData:hasType(BoxType.woodBox) end --根据index获取行列 function BombermanMgr:getIndexRow(index) return math.ceil(index/rowNum) end function BombermanMgr:getIndexCol(index) local col = index%rowNum return col == 0 and rowNum or col end --获取格子数据 function BombermanMgr:getBoxData(index) return self.boxPool[index] end --根据位置获取格子index function BombermanMgr:getCellIndex(pos) local x = pos.x local y = pos.z local rowIndex = math.ceil((x - startX)/cellSize) local colIndex = math.ceil((y - startY)/cellSize) return (colIndex-1)*rowNum+rowIndex end --获取格子对应的坐标(取格子中心) function BombermanMgr:getBoxCenterPos(index) local row = self:getIndexRow(index) local col = self:getIndexCol(index) local x = startX+(col-1)*cellSize+(cellSize/2) --x是递加 local y = startY+(row-1)*cellSize+(cellSize/2) --y是递加 return Vector3(x,0,y) end --获取当前格子九宫格 function BombermanMgr:getSudokuBoxs(index) local lst = self:getRoundIndex(index) local boxLst = { } for k,v in pairs(lst) do local data = self:getBoxData(k) if data then table.insert(boxLst,data) end end return boxLst end --获取格子周围所有格子 function BombermanMgr:getRoundIndex(index) local lst = {} --top 没有上 r1r2r3 --bottom没有下 r6r7r8 --left没有左r1r4r6 --right没有右r3r5r8 local isTop = index<=rowNum local isBottom = index > (colNum-1)*rowNum local isLeft = index%rowNum == 1 local isRight = index%rowNum == 0 local filter = {} local r1 = index - rowNum - 1 local r2 = index - rowNum local r3 = index - rowNum + 1 local r4 = index - 1 local r5 = index + 1 local r6 = index + rowNum - 1 local r7 = index + rowNum local r8 = index + rowNum + 1 if isTop then filter[r1] = true filter[r2] = true filter[r3] = true end if isBottom then filter[r6] = true filter[r7] = true filter[r8] = true end if isLeft then filter[r1] = true filter[r4] = true filter[r6] = true end if isRight then filter[r3] = true filter[r5] = true filter[r8] = true end local result = {r1,r2,r3,r4,r5,r6,r7,r8} for i =1,#result do if filter[result[i]] == nil then lst[result[i]] = true end end lst[index] = true return lst end function BombermanMgr:isVaildMove(nowIndex,targetPos) local index = self:getCellIndex(targetPos) return index == nowIndex+1 or index == nowIndex-1 or index == nowIndex+rowNum*1 or index == nowIndex+rowNum-1 end --动态修改层级 --如果玩家当前格子有阻碍物 --设置当前阻碍物为role层 不与玩家发生碰撞 --在玩家更换格子之后 把层级设置回来 function BombermanMgr:changeLayer(index,layerName) local boxData = self:getBoxData(index) if boxData and boxData:getIsHinder() then for k,v in pairs(BoxHinderType) do local map = boxData:getTypeLst(k) if map then for m,n in pairs(map) do --m = uid n = true if n == true then EntityUtils:changeStaticRoleLayer(m, LayerMask.NameToLayer(layerName)) end end end end end end --阻碍物加载完成 function BombermanMgr:checkLayer(hinderIndex,uid) local playerIndex = -1 local role = EntityMgr:getRole(self:getMainPlayerId()) if role then playerIndex = role:getCellIndex() end if hinderIndex == playerIndex then --self:changeLayer(hinderIndex,‘Role‘) EntityUtils:changeStaticRoleLayer(uid, LayerMask.NameToLayer(‘Role‘)) end end -------------------------------以下缓存玩家信息--------------------------------- function BombermanMgr:addInfoListener(handler) self.infoHandler = handler--不需要清理 end function BombermanMgr:invokeInfoHandler() if self.infoHandler then self.infoHandler() end end --PPTMemberData (uid,name,serverName,job,transferJob) function BombermanMgr:addMember(id,msg) if self.memberPool[id] == nil then local dt = PPTMemberData(id,msg.nick_name,msg.serverName,msg.tempId,msg.lifeCount) self.memberPool[id] = dt self:invokeInfoHandler() end end function BombermanMgr:updateMemberHp(id,hp) local dt = self.memberPool[id] if dt then dt:setHp(hp) end self:invokeInfoHandler() end function BombermanMgr:getMemberLst() return self.memberPool end --rankPool function BombermanMgr:addRank(id,msg) if self.rankMap[id] == nil then local dt = PPTMemberRankData(id,msg.nick_name,msg.serverName) table.insert(self.rankPool,dt) --lst 排序 self.rankMap[id] = dt --map 快速查找 self:invokeInfoHandler() end end function BombermanMgr:updateRank(id,score) local dt = self.rankMap[id] if dt then dt:setScore(score) end table.sort(self.rankPool,function(m,n) return m:getScore()>n:getScore() end) self:invokeInfoHandler() end function BombermanMgr:getRankLst() return self.rankPool end function BombermanMgr:getScore(id) return self.rankMap[id] and self.rankMap[id]:getScore() or 0 end --以下排行榜相关 PPTRankData:__init(id,rank,nickName,score) function BombermanMgr:addTodayLst(msg) self.todayLst = { } local lst = msg.allReports if lst and #lst>0 then for i = 1,#lst do local rsp = lst[i] local dt = PPTRankData(rsp.livingId,rsp.rank,rsp.nickName,rsp.toalScore) table.insert(self.todayLst,dt) end end table.sort(self.todayLst,function(m,n) return m:getRank()<n:getRank() end) local rsp = msg.meInfos self.todayMine = PPTRankData(rsp.livingId,rsp.rank,rsp.nickName,rsp.toalScore) end function BombermanMgr:addYesTodayLst(msg) self.yestodayLst = { } local lst = msg.allReports if lst and #lst>0 then for i = 1,#lst do local rsp = lst[i] local dt = PPTRankData(rsp.livingId,rsp.rank,rsp.nickName,rsp.toalScore) table.insert(self.yestodayLst,dt) end end table.sort(self.yestodayLst,function(m,n) return m:getRank()<n:getRank() end) local rsp = msg.meInfos self.yestodayMine = PPTRankData(rsp.livingId,rsp.rank,rsp.nickName,rsp.toalScore) end function BombermanMgr:getTodayLst() return self.todayLst,self.todayMine end function BombermanMgr:getYesTodayLst() return self.yestodayLst,self.yestodayMine end function BombermanMgr:canReqToday() return TimerMgr:getServerTime() - self.todayReqTime > 60*1000--60S才能拉取 end function BombermanMgr:canReqYesToday() return TimerMgr:getServerTime() - self.yestodayReqTime > 60*1000--60S才能拉取 end function BombermanMgr:resetReqToday() self.todayReqTime = TimerMgr:getServerTime() end function BombermanMgr:resetReqYesToday() self.yestodayReqTime = TimerMgr:getServerTime() end function BombermanMgr:getRankRewards() if self.rankRewardLst == nil then self.rankRewardLst = {} local list = RewardConfigMgr:getRewardConfigList(RewardType.PPTAllRank) if list then for i = 1, #list do local data = DemonAwardData(list[i]) self.rankRewardLst[#self.rankRewardLst + 1] = data end else Logger:logError("can not find RewardConfig type =", RewardType.PPTAllRank) end end return self.rankRewardLst end --出生点模型 BombermanConst.myRespawnModeltId function BombermanMgr:checkMyRespawnModel(idx) if not self.myRespawnRoleId then local pos = self:getBoxCenterPos(idx) --data self.myRespawnRoleId = MathUtils:getUniqueID() local roleData = LRoleData(self.myRespawnRoleId, RoleType.BoxHinder, true) roleData:changeAttr(RoleAttr.SkinId,BombermanConst.myRespawnModeltId) roleData:changeAttr(RoleAttr.isActive,true) roleData:changeAttr(RoleAttr.InitPosition, pos) roleData:setFull() roleData:attach() --创建 local staticEntity = LBoxHinderEntity(self.myRespawnRoleId, RoleType.BoxHinder, roleData , nil) if staticEntity then StaticEntityMgr:addRoleData(roleData) StaticEntityMgr:addRole(staticEntity) staticEntity:create() end end end --是否可以释放 function BombermanMgr:canCastBomb(isAlert) local role = EntityMgr:getRole(self:getMainPlayerId()) if role then local roleIndex = role:getCellIndex() local boxData = self:getBoxData(roleIndex) if boxData and boxData:hasType(BoxType.bombBox) then if isAlert then --1000ms才能弹出1次提示 if TimerMgr:getServerTime() - self.lastAlertTime > 800 then self.lastAlertTime = TimerMgr:getServerTime() UIUtils:floatAlertByKey(BombermanLang.thisBoxHasBombStr) end end return false end end return true end function BombermanMgr:isFollowPlayer(id) return id == self.followId end --格子是否可以实例化爆炸特效 function BombermanMgr:canPlayCellEffect(index,time) if not self.bombEffectTimeMap[index] or time - self.bombEffectTimeMap[index] > 500 then self.bombEffectTimeMap[index] = time return true end return false end --接口 index1是否在index2的 14*14范围内 用于播放音效 function BombermanMgr:indexIn14Range(mine,target) local mineRow = self:getIndexRow(mine) local mineCol = self:getIndexCol(mine) local tarRow = self:getIndexRow(target) local tarCol = self:getIndexCol(target) return math.abs(tarRow-mineRow) <= 7 and math.abs(tarCol-mineCol) <= 7 end --封装一层 音效播放 function BombermanMgr:playAudio(audioId, callBack) if audioId and audioId > 0 and self:canPlayAudioTime(audioId) then AudioMgr:playAudio(audioId, callBack) end end function BombermanMgr:canPlayAudioTime(audioId) local time = TimerMgr:getServerTime() local mill = AudioIdMinMill[audioId] and AudioIdMinMill[audioId] or 0 if not self.audioTimeMap[audioId] or time - self.audioTimeMap[audioId] > mill then self.audioTimeMap[audioId] = time return true end return false end --合并 function BombermanMgr:addStaticId(id) --table.insert(self.combineLst,id) self.combineLst:Add(id) if self.combineLst.Count>=1 then --combine EntityUtils:combineStatic(self.combineLst) self.combineLst:Clear() end end ---[[ function BombermanMgr:addToStatic(id) if self.combineRoot == nil or not Helper:goIsHas(self.combineRoot) then self.combineRoot = GameObject("combineRoot") end EntityUtils:setStaticParent(id,self.combineRoot) self.combineNum = self.combineNum + 1 if self.combineNum >= 15 then if self.combineRoot ~= nil and Helper:goIsHas(self.combineRoot) then --Logger:logError("combineRoot") StaticBatchingUtility.Combine(self.combineRoot) self.combineNum = 0 end end end --]] -- 清理数据 function BombermanMgr:clearSelf() self.boxPool = nil self.mainRoleId = 0 self.dropPool = { } --缓存掉落物 BUFF self.memberPool = { } --玩家信息 PPTMemberData self.rankPool = { }--玩家信息 PPTMemberRankData self.rankMap = { }--玩家排名 PPTMemberRankData self.todayReqTime = -1 --上次拉取时间 self.yestodayReqTime = -1 --上次拉取时间 self.todayLst = { } --今天数据 self.yestodayLst = { }--昨天数据 self.todayMine = nil --自己今天 self.yestodayMine = nil --自己昨天 self.myRespawnRoleId = nil --出生点模型 self.followId = -1 --跟随id self.lastAlertTime = -1--上次弹出提示时间 self.bombEffectTimeMap = { }--格子爆炸特效上次释放时间 self.audioTimeMap = { }--音效上次播放时间 --self.combineLst = ListLong() self.combineNum = 0 end ---------以下段落实现一些需要的定义方法-------end------------------------- -- 自动初始化 create(BombermanMgr)
<3>代码备注
--[[ 实体创建: 5,6号协议主角还是会创建(进入不会推) 在进入这个场景的时候,创建完毕炸弹人实体(继承LRole),相机进行跟随 同时关闭所有原先RootUI,摇杆重新创建一个新的(复用预设,class重写),技能UI也做新的(全部重写) 头像 任务UI都重做 位置同步: 客户端A摇杆拖动开始同步 停止移动也同步一次 客户端A每次预测当前方向0.1秒位置,预测位置发送给服务器,服务器进行广播 客户端A每次移动都检测是否碰撞到阻挡格子 客户端B接到同步协议,进行插值移动 怪物位置同步: 服务器每次推送怪物目标格子 客户端把每个格子位置加入到怪物的目标位置列表 客户端进行平滑插值 速度会根据列表中的个数进行加速度 碰撞器: 碰撞器使用Unity自带碰撞 如果玩家卡到格子里面 那么格子里面的物体碰撞层都会设置成不与玩家碰撞 触发器: 不适用Unity触发器 在Lua这边维护触发 主角每次改变格子 都会检测触发(检测当前格子和九宫格,盒子生成也需要与玩家所在格子检测) BUFF: buff存在于实体身上(对应关系备注在BombermanConst) buff服务器只告诉结束时间戳 客户端自己维护BUFF结束 无法使用之前的buff组件 实体获取buff起定时器 每100ms检查buff是否结束 使用setCountDown定时器会有后台运行的问题(后台运行定时器暂停) 主角buff定时器每次都抛出消息给UI 技能: 技能分为3中类型 1炸弹(可以充能) 2符文(使用一层少一层) 3血量(死亡一次少一次) 技能数据都存在于实体身上(对应关系备注在BombermanConst) 1炸弹充能 有最大充能次数(会动态更改) 2符文 3血量 其他: 1.进入PPT场景主动拉取所有信息(不然会出现还没加载完场景 协议来了创建实体 实体又马上被销毁) 2.进入PPT场景 服务器还是会推其他玩家的6号协议(服务器通用处理) 客户端PPT玩家实体的id = id*10 取实体的 更新实体都需要*10 3.玩家实体创建协议与怪物实体创建协议不同(怪物实体用的是Box协议 客户端转实体) --]]
标签:tcl rdd model war box uid class 初始化 lan
原文地址:https://www.cnblogs.com/cocotang/p/10825682.html