feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
+279
View File
@@ -0,0 +1,279 @@
///////////////////////////////////////////////////
/////// cls_youle_erqiwang_export: 输出接口 ///////
///////////////////////////////////////////////////
//跨模块依赖:Node 走 require 守卫;友乐/浏览器由 min_loadJsFile 加载为同名全局(dev-guide 01 §1)
if (typeof require !== "undefined"){
var cls_youle_erqiwang_config = require("./class.config.js");
var cls_youle_erqiwang_arith = require("./class.arith.js");
var cls_youle_erqiwang_desk = require("./class.desk.js");
var cls_youle_erqiwang_paiju = require("./class.paiju.js");
}
var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
new: function() {
var exp = {};
//解析roomtype获取开房所需房卡(design §10.1)
exp.get_needroomcard = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
if (cfg.deductAA){
//AA每人扣卡:6局每人1张 / 12局每人2张
return cfg.aset12 ? 2 : 1;
}
//房主扣卡:6局2张 / 12局4张
return cfg.aset12 ? 4 : 2;
}
//解析roomtype获取总局数(design §10.1:6局 / 12局)
exp.get_asetcount = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
return cfg.aset12 ? 12 : 6;
}
//加入房间时根据roomtype获取需要的房卡数量
exp.get_needroomcard_joinroom = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
if (cfg.deductAA){
//AA每人扣卡:加入者按自己那份扣(6局1张 / 12局2张)
return cfg.aset12 ? 2 : 1;
}
//房主扣卡:加入者不扣
return 0;
}
//开战
exp.makewar = function(o_room){
//牌桌对象
o_room.o_desk = cls_youle_erqiwang_desk.new(o_room);
for (var i = 0; i < o_room.seatlist.length; i++) {
o_room.seatlist[i].gameinfo.isbet = 1;
}
//开始第一局
var do_newpaiju = function(){
o_room.o_desk.method.do_new_paiju(0);
}
min_ontimeout(do_newpaiju, 1000);
}
//获取某个玩家的牌桌整桌当前信息
exp.get_deskinfo = function(o_room, seat){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
//本局房间可选规则:爬坡改变「叫分 → 基础子数」的取值(design §7.3.1),
//各阶段下发的 multiple 必须与结算包 aset.multiple 用同一套规则
var cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
var deskinfo = {};
deskinfo.count = o_room.asetcount; //总局数
deskinfo.idx = paiju.idx; //当前局数
deskinfo.PlayerInfo = []; //玩家当前累积得分
for (var i = 0; i < o_room.o_desk.seatlist.length; i++) {
deskinfo.PlayerInfo.push(o_room.o_desk.seatlist[i][0]);
};
deskinfo.step = paiju.step; //牌局状态
deskinfo.MyCards = paiju.method.get_seat_cards(seat); //我手上的牌
switch (paiju.step){
case 1: //发完牌叫分
deskinfo.CallRun = {};
//当前叫分位置
deskinfo.CallRun.seat = paiju.method.get_callgrade_seat();
//叫分倒计时
deskinfo.CallRun.countdown = o_room.o_desk.method.get_countdown_jiaofen();
//当前叫分
deskinfo.CallRun.nowcall = paiju.method.get_callgrade_value();
//当前叫分倍数
deskinfo.CallRun.multiple = cls_youle_erqiwang_arith.get_base_bycall(deskinfo.CallRun.nowcall, cfg.climb);
//所有玩家的叫分情况
deskinfo.CallRun.call = [null, null, null];
deskinfo.CallRun.call[0] = paiju.method.get_seat_callgrade(0);
deskinfo.CallRun.call[1] = paiju.method.get_seat_callgrade(1);
deskinfo.CallRun.call[2] = paiju.method.get_seat_callgrade(2);
break;
case 2: //选主
deskinfo.ChooseMain = {};
//控制权:选主/投降都只由庄家做(mod.xuanzhu / mod.touxiang 的 SEAT 校验以 banker 为准)。
//与 shangzhuang 推送的 nextseat 同源同值——两条路径都由服务端算好给出,
//前端不再自己套「选主 = 庄家」这条规则(字段名与 CallRun.seat 一致:分组内的 seat = 轮到谁)
deskinfo.ChooseMain.seat = paiju.banker;
//庄家
deskinfo.ChooseMain.banker = paiju.banker;
//庄家叫分
deskinfo.ChooseMain.call = paiju.call;
//庄家叫分倍数
deskinfo.ChooseMain.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//选主倒计时
deskinfo.ChooseMain.countdown = o_room.o_desk.method.get_countdown_xuanzhu();
//底牌(发牌时没发给玩家、扣在桌面的 8 张):只有庄家可见(70分的3秒亮牌是上庄时的一次性事件,重连不重放)
if (seat == paiju.banker){
deskinfo.ChooseMain.bottomcards = paiju.method.get_bottomcards();
}
//是否可投降
if (paiju.call != 70){
deskinfo.ChooseMain.touxiang = 0;
} else {
deskinfo.ChooseMain.touxiang = 1;
}
//当前抓分倍数(design §7.2.0):与 shangzhuang 推送同源同值。
//选主阶段一张牌都还没出,捡分恒为 0 → 必为 +3(大光);
//漏发会让重连后顶部「抓分」角标掉回 0,与增量路径不一致
deskinfo.ChooseMain.curmultiple = paiju.method.get_curmultiple();
break;
case 3: //埋牌
deskinfo.BuryCards = {};
//控制权:埋牌只由庄家做(mod.maipai 的 SEAT 校验以 banker 为准)。
//与 xuanzhu 推送的 nextseat 同源同值
deskinfo.BuryCards.seat = paiju.banker;
//庄家
deskinfo.BuryCards.banker = paiju.banker;
//庄家叫分
deskinfo.BuryCards.call = paiju.call;
//庄家叫分倍数
deskinfo.BuryCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//埋牌倒计时
deskinfo.BuryCards.countdown = o_room.o_desk.method.get_countdown_maipai();
//底牌(发牌留桌的 8 张):只有庄家可见
if (seat == paiju.banker){
deskinfo.BuryCards.bottomcards = paiju.method.get_bottomcards();
}
//主牌花色
deskinfo.BuryCards.flower = paiju.flower;
//当前抓分倍数(design §7.2.0):埋牌阶段同样一张牌未出,捡分恒为 0 → 必为 +3
deskinfo.BuryCards.curmultiple = paiju.method.get_curmultiple();
//埋牌阶段无投降(投降是选主阶段与选主互斥的选择)
break;
case 5: //出牌
deskinfo.PushCards = {};
//庄家
deskinfo.PushCards.banker = paiju.banker;
//庄家叫分
deskinfo.PushCards.call = paiju.call;
//庄家叫分倍数
deskinfo.PushCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//出牌倒计时
deskinfo.PushCards.countdown = o_room.o_desk.method.get_countdown_chupai();
//主牌花色
deskinfo.PushCards.flower = paiju.flower;
//捡分
var jian_grade = paiju.method.get_jian_grade();
if (seat == paiju.banker){
//埋牌底牌(庄家埋下的 8 张)。与「底牌」(发牌留桌 8 张,见 ChooseMain/BuryCards.bottomcards)
//是两批不同的牌,字段名刻意区分,勿混用
deskinfo.PushCards.burycards = paiju.method.get_burycard();
} else {
//闲家当前的捡分分牌
deskinfo.PushCards.gradecards = jian_grade.cards;
}
//闲家当前的捡分分数
deskinfo.PushCards.grade = jian_grade.grade;
//当前抓分倍数(design §7.2.0):与 chupai 包同源同值,重连后角标立即正确
deskinfo.PushCards.curmultiple = paiju.method.get_curmultiple();
//当前的出牌情况:与 chupai1/2/3 共用同一处快照函数(结构一致、且是深拷贝,
//不把活对象挂进下发包,见 class.paiju.js get_playproc)
deskinfo.PushCards.playproc = cls_youle_erqiwang_paiju.get_playproc(paiju);
//本轮跟牌的必出牌(design §5.2):只在【轮到本座位】且本轮已有人出牌时才有,
//算的也只是本座位自己的手牌,不涉及他家(server 红线:按可见性下发)
if (paiju.playproc && paiju.playproc.currseat == seat){
var _must = paiju.method.get_mustcard(seat);
if (_must){
deskinfo.PushCards.mustcard = _must;
}
}
//报无主:与 chupai1/2/3 的 baozhu 同源同门控(mod.chupai)。
//【必须带】它是「余主公示 + 明牌按钮」的开关(design §9,前端
//Sprites_Table.js 的明牌按钮据此显隐)。只在 chupai 包里给、重连包不给,
//等于重连后按钮凭空消失——状态变更没有包承载(server 红线:发全下发面)。
//不查牌模式恒 0(与 chupai 一致,协议 §11–§13 baozhu 条目)
deskinfo.PushCards.baozhu = (!cfg.nocheck && paiju.method.have_baofu()) ? 1 : 0;
//座位列表(报副统计)与亮牌:仅可查牌模式下展示(§9/§8.2)
if (!cfg.nocheck){
//【深拷贝】与 maipai / chupai1/2/3 共用同一处快照函数,且不把活表挂进下发包
//(paiju.seatlist 是 do_playcard 每手就地改写的活对象,见 class.paiju.js get_seatlist)
deskinfo.PushCards.seatlist = cls_youle_erqiwang_paiju.get_seatlist(paiju);
if (seat != paiju.banker){
var _lp = paiju.method.get_liangpai();
if (_lp){
deskinfo.PushCards.liangpai = _lp;
}
}
}
//出牌历史:往轮打出、已被收走的牌属于「查牌」范畴,仅可查牌模式下发(design §9)。
//不查牌模式下不带此属性——当前这一轮桌面上的牌由上面的 playproc 恢复,两种模式都有,
//否则后出的人无从跟牌(design §9 末尾的边界说明)
if (!cfg.nocheck){
//【直读归档、不重建】pushlist 的唯一来源是 do_playcard 出牌当场写下的
//paiju.playhistory(见 class.paiju.js)。旧实现是从 paiju.cards 的 playround/dealowner
//反查再自己排一次序——那是同一份数据「这一手打出的牌」的第二个写入处:
//一手多张(甩牌/对子/拖拉机)时它与 chupai1/2/3 的 data.cards 会给出不同顺序,
//前端增量回放与重连重建因此对不上(同编码的一对牌尤其:排序无法区分它们)。
deskinfo.PushCards.pushlist = paiju.method.get_pushlist();
}
break;
case 6: //结算
deskinfo.Balance = {};
//玩家的准备状态
//【返回副本】o_room.o_desk.prepare 是一张全程就地改写的活数组
//(do_prepare 每次准备就地把某一格置 1,do_new_paiju 又就地重建),
//把它本体挂进下发包会让「已发出的包」随后续准备操作被回改。
//与 get_playproc / get_seatlist / get_pushlist / get_bottomcards 同一条标准:
//平台真实链路虽然立刻序列化看不出来,但这是隐患,不留。
deskinfo.Balance.readystate = o_room.o_desk.prepare.concat();
if (o_room.o_desk.prepare[seat] == 0){
//结算面板的三件套:与 jiesuan 推送【同源同值】(都取 get_paiju_account 末尾
//冻结的那份快照)。只给 aset 会让「结算面板还开着时断线重连/硬刷新」丢掉
//抠底明细与末局大结算——增量路径有、重连路径空白,正是「同一份数据两条路径
//给的不一样」(server 红线:发全下发面,漏发修在服务端发包处)。
//结算情况
deskinfo.Balance.aset = paiju.tmp_jiesuan_aset;
//抠底明细:仅正常出牌结算存在(投降/解散没有抠底这一步),与 jiesuan.bottom 同一份
if (paiju.tmp_jiesuan_bottom){
deskinfo.Balance.bottom = paiju.tmp_jiesuan_bottom;
}
//大局结算:仅末局或解散时存在,与 jiesuan.account 同一份。
//「有就带、没有就不带」与 jiesuan 推送的取舍完全一致,前端两条路径同一套解析
if (paiju.tmp_jiesuan_account){
deskinfo.Balance.account = paiju.tmp_jiesuan_account;
}
}
break;
}
return deskinfo;
}
exp.createroom_needprepare = function(roomtype, o_game_config) {
return 1; // 1 = 需要准备
};
//解散房间
//平台在 battlestate==1 时即会调用(rpc.js 开战处 makewar 后立刻置 1),而本游戏的首个牌局
//是 makewar 里 min_ontimeout(...,1000) 延迟创建的——这段窗口内 o_desk 已存在但 paiju_list 为空。
//此时返回 null(平台对 null 的 _deskfree 有分支:照常发解散包、不带 deskfree),
//而不是让 curr_paiju() 的 undefined 解引用抛异常打断整条解散链路。
exp.get_disbandRoom = function(o_room){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
if (!paiju){
return null;
}
var msg = {};
return paiju.method.get_paiju_account(2, msg);
}
return exp;
}
}
//对内输出接口(友乐/浏览器:youle_erqiwang 由 mod.js 先建;Node 单测无此全局时跳过)
if (typeof youle_erqiwang !== "undefined"){
youle_erqiwang.export = cls_youle_erqiwang_export.new();
}
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)
if (typeof module !== "undefined"){
module.exports = cls_youle_erqiwang_export;
}