夜雨聆风学习资料网

ARTICLE · 1006294

三国霸业钩子函数参考文档

三国霸业钩子函数参考文档
文档根据 @loong 编写的手册及开发文档、乱战三国系列脚本、源码使用WorkBuddy 软件生成。仅供各位开发人员参考。

0. 钩子机制原理

引擎(Emscripten 编译的 WASM)在特定时机通过桥接函数回调 JS:
// js/baye.js:1459 —— C++ 侧调用入口2594649function($0, $1) {    var name = UTF8ToString($0);    var rv = 0;    if (window.baye == undefined || window.baye.hooks == undefined        || window.baye.hooks[name] == undefined) {        rv = -1;                                  // 未注册 → 返回 -1,引擎走默认逻辑    } else {        var cContext = $1;        if (cContext != 0) {            var jsContext = baye_bridge_value(cContext);            rv = baye.callHook(name, jsContext);  // 有上下文        } else {            rv = baye.callHook(name, undefined);  // 无上下文        }    }    return rv;}
// js/bridge.js:387baye.callHook = function(name, context) {    var rv = window.baye.hooks[name](context);    baye.pushCallback(baye.callback);    return rv;};

三条铁律

规则
说明
① 未注册 = 走默认
钩子不存在时引擎返回 -1,游戏行为与原生完全一致。可以放心只挂需要的钩子。
② 返回值语义
多数"可拦截型"钩子:return 0 = 已处理,跳过引擎默认逻辑return -1(或不返回)= 交给引擎处理;部分钩子 return 1 = 提示错误。务必看每个钩子的说明。
③ ctx 是双向的
ctx
 由 WASM 内存映射而来,读字段拿输入,写字段就是输出。改完直接生效,不需要 return 对象。

1. 游戏生命周期类

1.1 didOpenNewGame

内容
调用时机
新开一局游戏后
参数
返回值
无意义
典型用途
初始化自定义数据(customData)、重置存档外挂字段、给开局将领发装备
baye.hooks.didOpenNewGame = function() {    // 初始化自定义全局数据    customData = {};    customData.Interior = [0000000000];    // 给所有将领写入扩展属性容器    for (var i = 0; i < baye.data.g_Persons.length; i++) {        baye.data.g_Persons[i].ext = { ExtraAttr: lzsg.makePersonExtra() };    }    // 开局执行一次月度结算,避免首月数据为空    baye.hooks.tacticStage5();};

📌 LZSG 实战:在 didOpenNewGame 里主动调用 tacticStage5() 完成开局数据铺设。

✅ 官方示例(exscript/扩展属性存档.js)— 扩展数据「初始化 + 挂载」两段式
官方推荐把「造数据」和「挂引用」拆成两个函数,因为读档时只需 link 不需 init:
var customData = {};// ① 造数据:只在开局调用function initCustomData() {    customData.extPersons = [];    var total = baye.data.g_Persons.length;    for (var pid = 0; pid < total; pid++) {        var ext = {};        ext.lucky = 10;                      // 扩展字段:吉运值        customData.extPersons.push(ext);    }}// ② 挂引用:把扩展数据绑到引擎对象上(开局和读档都要调用)function linkCustomData() {    var arr = baye.data.g_Persons;    for (var i = 0; i < arr.length; i++) {        arr[i].ext = customData.extPersons[i];    }}baye.hooks.didOpenNewGame = function() {    initCustomData();      // 开局:造数据    linkCustomData();      // 开局:挂引用};

💡 为什么要拆开didLoadGame 时数据是从存档 JSON 还原的,再 init 会把玩家进度覆盖掉——那时只调 linkCustomData()


1.2 willSaveGame

内容
调用时机
执行存档
参数
返回值
无意义
典型用途
把数据写进引擎的存档槽位(baye.data.g_** 才会被持久化)
baye.hooks.willSaveGame = function() {    // 只有挂在 baye.data 上的数据才会被存档    // customData 中无法序列化的部分需先摊平到 g_ 字段    for (var i = 0; i < baye.data.g_Persons.length; i++) {        var P = baye.data.g_Persons[i];        P.Ext = P.Ext || {};        P.Ext.ExtraAttr = lzsg.flattenExtra(P.ext.ExtraAttr);    }};

⚠️ 存档关键customData 是脚本局部变量,引擎不认识。想持久化要么写回 baye.data.g_Persons[i].Ext.ExtraAttr 这类引擎字段,要么用官方的 baye.setCustomData() 通道(见下方官方示例)。

✅ 官方示例 A(exscript/扩展属性存档.js、显示版本示例.js)— 用setCustomData存整个 JSON
这是官方推荐的通道:引擎额外开一块自定义存档区,脚本自己管序列化。
var customData = {};baye.hooks.willSaveGame = function() {    var data = JSON.stringify(customData);    baye.setCustomData(data);        // 写进引擎的自定义存档区};
配套读档(didLoadGame)见 1.3。
✅ 官方示例 B(exscript/减小存档空间方式之一.js)— 大数组编码压缩
AddTools 这类对象数组直接 JSON.stringify 会存下大量重复的 key 名。官方给的招数是存成二维数组,用一份字段定义表压缩:
// 字段定义表:数组下标 <-> 对象属性名 的映射,存档和读档共用同一份baye.AddToolsDEFINE = [”belong”,”tooltype”,”tooltlevel”,”raritylv”,”durability”, /* ... */];// 对象数组 -> 二维数组(省掉 key 名)baye.arrayEncode = function(def, array) {    return array.map(function(item){        var rst = [];        for (var i = 0; i < def.length; i++) { rst.push(item[def[i]]); }        return rst;    });};baye.arrayDecode = function(def, array) {   // 二维数组 -> 对象数组    return array.map(function(item){        var rst = {};        for (var i = 0; i < def.length; i++) { rst[def[i]] = item[i]; }        return rst;    });};baye.hooks.willSaveGame = function() {    var tmpData = baye.copy(customData);            // 浅拷贝,避免污染运行时数据    tmpData[”AddTools”] = baye.arrayEncode(baye.AddToolsDEFINE, tmpData[”AddTools”]);    baye.setCustomData(JSON.stringify(tmpData));};

⚠️ def 定义表只能往后追加字段,不能插入或改顺序——否则老存档解码后字段会整体错位。要改结构就得做版本迁移。


1.3 didLoadGame

内容
调用时机
成功读档
参数
返回值
无意义
典型用途
从引擎字段还原脚本运行时结构、版本迁移
baye.hooks.didLoadGame = function() {    // 从引擎字段还原为脚本易用的对象形态    for (var i = 0; i < baye.data.g_Persons.length; i++) {        var P = baye.data.g_Persons[i];        P.ext = { ExtraAttr: lzsg.expandExtra(P.Ext.ExtraAttr) };    }    // 老存档补字段(迁移)    lzsg.migrateSaveVersion();};
✅ 官方示例(exscript/扩展属性存档.js、显示版本示例.js)
baye.hooks.didLoadGame = function() {    var rawJson = baye.getCustomData();          // 取出 willSaveGame 存的字符串    var dat = JSON.parse(rawJson);    customData = dat ? dat : {};                 // 兼容首次开局(无存档 → 空对象)    linkCustomData();                            // 只需挂引用,不能 init(会覆盖进度)};

⚠️ 必坑点:g_Persons 数组长度会串读档后引擎里的 g_Persons 长度可能还残留上一局的值,官方示例第一行就是修这个:

baye.data.g_Persons.length = _bayeGetPersonCount();   // 先校正长度再遍历

_bayeGetPersonCount() 是 WASM 导出的原始函数,对外暴露的名字是 baye.getPersonCount()js/bridge.js:373),两者等价。漏掉这行会导致遍历到野数据。

✅ 官方示例:带版本迁移(exscript/显示版本示例.js)
存档结构变了要能识别老档,官方做法是开局时把版本号写进 customData:
var libversion = ”1.2.3.4”;var customData = {};baye.hooks.didOpenNewGame = function() {    customData.libversion   = libversion;                  // 记住”这局是用哪个版本开的”    customData.engineVersion = baye.data.g_engineVersion;};baye.hooks.didLoadGame = function() {    var dat = JSON.parse(baye.getCustomData());    customData = dat ? dat : {};    if (customData.libversion != libversion) {        lzsg.migrateSaveVersion(customData.libversion);    // 版本不一致 → 迁移    }};

1.4 loadPeriod

内容
调用时机
开局选择时期(剧本)时
参数
返回值
0
 = 返回主界面;1 = 继续选择君主
典型用途
自定义剧本/难度选择界面
baye.hooks.loadPeriod = function() {    baye.centerChoose(10080, [”简单”, ”困难”], 0function(idx) {        if (idx == baye.None) {            return 0;              // 取消 → 回主界面        }        baye.loadPeriod(1);        // 加载时期 1 的数据        lzsg.difficulty = idx;     // 记录难度供后续使用        return 1;                  // 继续选君主    });};

1.5 chooseGameEntry

内容
调用时机
游戏主菜单(新开局 / 重返沙场 / 退出)
参数
返回值
原菜单选项序号
典型用途
改写主菜单项、插入自定义入口
baye.hooks.chooseGameEntry = function() {    baye.centerChoose(10050, [”新开局”, ”重返沙场”, ”退出”], 0function(idx) {        if (idx == 2) {            return 3;              // 重映射:第3项映射到引擎的”退出”(3)        }        return idx;    });};

1.6 chooseActor

内容
调用时机
开局选择君主(势力)时
参数
返回值
选中的人物 ID;baye.None = 取消
典型用途
限制可选君主、自定义势力列表
baye.hooks.chooseActor = function() {    var ids = [123];                                // 只允许这 3 个君主    var names = ids.map(function(x) { return baye.getPersonName(x); });    baye.centerChoose(10050, names, 0function(idx) {        if (idx == baye.None) { return baye.None; }        return ids[idx];                                // 返回真实人物 ID    });};

1.7 choosingActorUpdate

内容
调用时机
势力选择界面刷新时(光标移动到某君主)
参数
ctx
 — 当前选中项信息
返回值
无意义
典型用途
显示该势力的预览信息(难度、城池数、名将数)
baye.hooks.choosingActorUpdate = function(ctx) {    // 在选中项变化时绘制势力介绍浮窗    var pid = ctx.index;    lzsg.drawKingPreview(pid);};

2. 策略阶段类(内政回合)

引擎每月按固定顺序执行 5 个策略阶段,是挂自定义月度逻辑的主战场。
tacticStage1 → tacticStage2 → tacticStage3 → tacticStage4 → tacticStage5 
钩子
时机(LZSG 实战标注)
典型用途
tacticStage1
月初 Data 更新
刷新本月额度、重置每月计数、结算上月遗留
tacticStage2
策略阶段 2
AI 内政决策前
tacticStage3
玩家操作结束后执行任务
处理本月下达的命令队列
tacticStage4
月末 Data 更新
AI 升级、势力结算、战报汇总
tacticStage5
月末 Data 更新(最后)
存档数据铺设、排行榜、天赋结算
共同点:全部无参数、无返回值,纯自由逻辑。
⚠️ 引擎没有内置动画 API — 官方参考实现
引擎只提供 baye.delay() 定时回调,没有补间/缓动/帧动画封装。官方给了两个参考实现(exscript/线性动画参考实现.js、线性动画参考实现2.js),直接挂到 baye.linearAnimation 上用:
/* 线性动画参考实现 from      起始坐标 [x, y] to        终止坐标 [x, y] duration  动画持续时间(毫秒) frames    总帧数 draw      单帧绘图函数 draw(x, y) end       动画结束回调(可选)*/baye.linearAnimation = function(from, to, duration, frames, draw, end) {    var frame_time = duration / frames;    function nextFrame(n) {        var x = from[0] + (to[0] - from[0]) * n / frames;        var y = from[1] + (to[1] - from[1]) * n / frames;        draw(x, y);        if (n < frames) {            baye.delay(frame_time, 0function(){ nextFrame(n+1); });        } else if (end) {            end();                                   // 结束回调        }    }    nextFrame(0);};// 挂在策略阶段 2 上跑一个小方块从左上滑到中间baye.hooks.tacticStage2 = function() {    baye.linearAnimation([00], [8060], 10020function(x, y) {        baye.clearRect(x, y, x+4, y+4);        baye.drawRect(x, y, x+4, y+4);    });};
配合saveScreen/restoreScreen做无闪烁动画(线性动画参考实现2.js)
上面那版每帧 clearRect 会擦掉背景。官方第二版改成先存屏、每帧还原再画,适合在现有界面上叠加动画:
baye.hooks.showMainHelp = function() {    baye.saveScreen();                                 // ① 先把当前画面存起来    baye.linearAnimation([00], [8060], 6020,        function(x, y) {            baye.restoreScreen();                      // ② 每帧先还原背景            baye.clearRect(x, y, x+4, y+4);            baye.drawRect(x, y, x+4, y+4);        },        function() {            baye.restoreScreen();                      // ③ 结束时再还原一次,清掉残影        }    );};

📌 相关 APIbaye.delay(毫秒, ?, 回调) — 定时执行;baye.saveScreen() / baye.restoreScreen() — 存取整个屏幕缓冲;baye.clearRect(x0,y0,x1,y1) / baye.drawRect(x0,y0,x1,y1) — 清矩形 / 画矩形边框。

⚠️ 动画期间引擎仍在跑自己的循环,长动画建议配合状态锁,避免玩家操作打断导致画面错乱。

示例:每月让 AI 势力将领按概率升级(官方范例)
baye.hooks.tacticStage4 = function() {    var allPerson = baye.data.g_Persons;    var playerKingId = baye.data.g_PlayerKing + 1;    var maxLevel = baye.data.g_engineConfig.maxLevel;    for (var i = 0; i < allPerson.length; i++) {        var p = allPerson[i];        if (p.Belong > 0 && p.Belong != playerKingId            && p.Belong != 255 && p.Level < maxLevel) {            if (Math.random() < 0.7) {         // 70% 概率升级                p.Level = p.Level + 1;            }        }    }};
示例:月初重置每月额度
baye.hooks.tacticStage1 = function() {    for (var c = 0; c < baye.data.g_Cities.length; c++) {        var Ext = baye.data.g_Cities[c].Ext.ExtraAttr;        Ext[7].monthlyBusiness = 5;   // 每月商业可开发次数        Ext[7].monthlyFarm     = 5;   // 每月农业可开发次数    }    // 重置将领的月度培养次数    for (var p = 0; p < baye.data.g_Persons.length; p++) {        baye.data.g_Persons[p].ext.ExtraAttr.cultivatedTimes = 0;    }};
tacticStageUser ⚠️ 非官方(WASM 实测存在)
内容
调用时机
玩家在策略阶段打开自定义菜单时
参数
返回值
无意义
来源
官方文档未记载,但 baye.wasm 中确认存在,LZSG 重度使用
baye.hooks.tacticStageUser = function() {    var PKingID = baye.data.g_PlayerKing;    // 自定义内政菜单:将领/装备/库房/俘虏/任务/商铺/城池    baye.chooseCity(function(city) {        if (city == 0xff) { return 0; }        var CData = baye.data.g_Cities[city];        if (CData.Belong == PKingID + 1) {            lzsg.PlayerMenu1(city, 0);     // 己方城池菜单        } else {            lzsg.RobotMenu1(city, 0);      // 敌方城池菜单        }    });};
✅ 官方示例(exscript/tactic-example.js)— 「选城 → 选命令」两级菜单
baye.hooks.tacticStageUser = function() {    function tactic() {        // 第一级:选城池        baye.chooseCity(function(city) {            if (city == 0xff) {          // 按了 Esc                return 0;            }            // 第二级:选命令            baye.centerChoose(5050, [”出征”, ”搜寻”, ”处斩”], 0function(i) {                if (i == 0) {            // 出征                    baye.makeBattle(city, function(){ tactic(); });                } else if (i == 1) {     // 搜寻(命令号 3)                    baye.makeCommand(city, 3function(){ tactic(); });                } else if (i == 2) {     // 处斩(命令号 7)                    baye.makeCommand(city, 7function(){ tactic(); });                } else {                    tactic();            // 继续策略                }            });        });    }    tactic();    return 0;};

💡 官方示例的两个关键手法

  1. 递归回调function tactic(){...} — 命令执行完再调回 tactic(),形成「选城→选命令→执行→再选城」的循环,玩家按 Esc 才退出。这是官方做自定义内政循环的标准写法。
  2. return 0
     在最后——注意 tactic() 是异步的(回调驱动),这个 return 0 是立即返回的,不是等菜单走完。
📋 命令 ID 宏定义表(官方示例注释中给出的 C 头文件常量,baye.makeCommand(city, id, cb) 用)
ID
常量
含义
分组
0
NOP
什么也不做
内政
1
ASSART
开垦
内政
2
ACCRACTBUSINESS
招商
内政
3
SEARCH
搜寻
内政
4
FATHER
治理
内政
5
INSPECTION
出巡
内政
6
SURRENDER
招降
内政
7
KILL
处斩
内政
8
BANISH
流放
内政
9
LARGESS
赏赐
内政
10
CONFISCATE
没收
内政
11
EXCHANGE
交易
内政
12
TREAT
宴请
内政
13
TRANSPORTATION
输送
内政
14
MOVE
移动
内政
15
ALIENATE
离间
外交
16
CANVASS
招揽
外交
17
COUNTERESPIONAGE
策反
外交
18
REALIENATE
反间
外交
19
INDUCE
劝降
外交
23
RECONNOITRE
侦察
军备
24
CONSCRIPTION
征兵
军备
25
DISTRIBUTE
分配
军备
26
DEPREDATE
掠夺
军备
27
BATTLE
出征
军备

📌 分组边界:ORDER_INTERIOR=1ORDER_DIPLOMATISM=15ORDER_ARMAMENT=23注意 20~22 是空档(原 C 里 TRIBUTE 朝贡 被注释掉了),别踩。


3. 命令与内政类

3.1 willAddOrder

内容
调用时机
即将下达某命令
返回值
0
 = 已处理,跳过引擎默认-1 = 交给引擎
ctx 参数:
字段
类型
说明
ctx.OrderId
U8
命令 ID
ctx.Person
U8
执行命令的人物
ctx.City
U8
执行命令的城池
ctx.Object
U8
目标人物 ID 或目标城池 ID
ctx.Arms
U16
兵力(运输)
ctx.Food
U16
粮草(运输)
ctx.Money
U16
金钱(运输)
ctx.Consume
U8
命令耗时(暂无用处)
ctx.TimeCount
U8
命令累时(暂无用处)
baye.hooks.willAddOrder = function(ctx) {    // 拦截”运输”类命令,自定义粮草上限    if (ctx.OrderId == 12) {                       // 假设 12 = 运输        var city = baye.data.g_Cities[ctx.City];        if (ctx.Food > city.Food * 0.5) {            ctx.Food = Math.floor(city.Food * 0.5); // 最多运走一半        }        return -1;                                  // 改完交给引擎执行    }    return -1;};

3.2 willExecuteOrder

内容
调用时机
即将执行某命令(与 willAddOrder 分处下达/执行两个时点)
参数
同 willAddOrder
返回值
0
 = 跳过引擎;-1 = 交给引擎
baye.hooks.willExecuteOrder = function(ctx) {    // 记录命令日志,不改变行为    lzsg.logOrder(ctx.OrderId, ctx.Person, ctx.City, ctx.Object);    return -1;};

3.3 cityMakeCommand

内容
调用时机
城池菜单选择某命令
返回值
0
 = 已处理跳过引擎;1 = 提示错误并跳过;-1 = 引擎默认
ctx 参数:ctx.cityIndex (U8)、ctx.commandIndex (U8)
baye.hooks.cityMakeCommand = function(ctx) {    var city = baye.data.g_Cities[ctx.cityIndex];    // 非己方城池禁止操作    if (city.Belong != baye.data.g_PlayerKing + 1) {        return 1;                  // 提示错误,跳过    }    return -1;                     // 正常城池交给引擎};

4. 战斗流程 — 阶段类

enterBattle → battleStage1 → ... → battleStage5 → exitBattle 

4.1 enterBattle

内容
调用时机
进入战场
参数
返回值
无意义
典型用途
战斗属性初始化、把 ExtraAttr 刷入 g_GenPos 战场数据
baye.hooks.enterBattle = function() {    // 为每个战场位置生成战斗数据    for (var pos = 0pos < baye.data.g_FgtParam.GenArray.length; pos++) {        lzsg.BattleAttrWrite(pos);    }};
✅ 官方示例(exscript/AOE示例.js、自定义战场地图.js)— 调试期强制摆位
改战场地图后,将领起始位置常常跑到山里/城外,官方用这个钩子把人摆回来:
baye.hooks.enterBattle = function() {    // [位置索引, x, y] —— 索引 0~9 是我方,10~19 是敌方    var layout = [        [0183], [1163], [2203], [3193], [4161],        [10173], [111516], [121415], [131615], [141514],    ];    for (var i = 0; i < layout.length; i++) {        var p = baye.data.g_GenPos[layout[i][0]];        p.x = layout[i][1];        p.y = layout[i][2];    }    baye.data.g_LookMovie = 0;              // 关掉”观战”模式};
把人拉到镜头中心(调试时用):
baye.hooks.enterBattle = function() {    baye.data.g_GenPos[10].x = baye.data.g_CityX;    baye.data.g_GenPos[10].y = baye.data.g_CityY;};

📌 相关全局量g_GenPos[i] 是可写的战场坐标表(.x / .y / .hp / .mp / .active / .state),g_LookMovie 置 0 表示玩家可控(非观战),g_FoucsX / g_FoucsY 是当前镜头中心(注意引擎拼写是 Foucs 不是 Focus)。


⚠️ 无效钩子:fightStage1
exscript/AOE示例.js 里出现了这个钩子:
baye.hooks.fightStage1 = function() {    baye.data.g_FgtWeather = 0;             // 强制晴天};
但它不是一个真实存在的钩子
baye.wasm
二进制中找不到fightStage1,连前缀 fightStage 都搜不到(0 命中);
作为对照,同一份示例里的 enterBattle、countSkillHurt、showSkill 全部命中;
官方手册、js/examples.js、js/demos.js 均无记载;
引擎真实的战场阶段钩子叫battleStage1~battleStage5(WASM 中确认存在)。
推测:作者想写 battleStage1,或者把 tacticStage 和 battleStage 记混了写成了 fightStage。引擎不会回调它,这行是死代码,天气根本改不掉。
正确写法:
baye.hooks.battleStage1 = function() {      // 注意是 battle 不是 fight    baye.data.g_FgtWeather = 0;             // 0=晴 1=雨 2=雪 …(具体枚举待实测)};

📌 这是本手册确认的第二个无效钩子(第一个是 didChangeMenuSelection)。两个都是第三方脚本作者的笔误,属于「写了不报错、但永远不会执行」的静默失效——凡是抄来的钩子名,建议先过一遍第 13 节的速查表确认存在性。


4.2 battleStage1 ~ battleStage5

内容
调用时机
战场阶段 1~5(回合内的五个子阶段)
参数
返回值
无意义
LZSG 实战
battleStage5
 用作回合结束,处理特效结算
baye.hooks.battleStage5 = function() {    // 回合结束:结算中毒/燃烧等持续状态    for (var i = 0; i < baye.data.g_GenPos.length; i++) {        lzsg.tickBuffState(i);    }};

4.3 exitBattle

内容
调用时机
战争结束
参数
返回值
无意义
典型用途
战斗结果处理、进攻队列推进、战利品结算
baye.hooks.exitBattle = function() {    // 清理解放军战场临时数据    lzsg.clearBattleTempData();    // 处理攻方进攻队列的下一条指令    lzsg.advanceAttackQueue();};

5. 战斗流程 — 数值计算类(最常用)

这类钩子是改写战斗规则的核心,全部支持"接管 or 放行"。

5.1 battleBuildAttackAttriutes ⚠️ 注意拼写

引擎原文拼写为 Attriutes(少了 b),不要写成 Attributes,否则不生效。

内容
调用时机
计算将领的攻防等属性时
返回值
0
 = 已处理跳过引擎;-1 = 引擎默认
ctx 参数:
字段
类型
说明
ctx.index
U8
计算结果存储位置(0=攻方,1=守方)
ctx.generalIndex
U8
将领的战场序号
输出:结果写入全局 baye.data.g_GenAtt[ctx.index]
baye.hooks.battleBuildAttackAttriutes = function(ctx) {    var P = baye.data.g_Persons[        baye.data.g_FgtParam.GenArray[ctx.generalIndex] - 1    ];    var Attr = P.ext.ExtraAttr;    var g = baye.data.g_GenAtt[ctx.index];    // 用自定义 ExtraAttr 覆盖引擎默认属性    g.Arms      = Attr.troops;    g.Force     = Attr.strength;    g.IQ        = Attr.intelligence;    g.PhysAtk   = Attr.physAtkBase;    g.PhysDef   = Attr.physDefBase;    g.MagAtk    = Attr.magAtkBase;    g.MagDef    = Attr.magDefBase;    g.MoveSpeed = Attr.moveSpeed;    return 0;          // 完全接管,不让引擎再算};

💡 可主动调用:这个钩子还能被脚本自己调用来做"实时试算":

baye.hooks.battleBuildAttackAttriutes({generalIndex: atk, index0});baye.hooks.battleBuildAttackAttriutes({generalIndex: def, index1}); 

5.2 countAttackHurt

内容
调用时机
计算普通攻击对兵力伤害时
返回值
0
 = 跳过引擎;-1 = 引擎默认
ctx 参数:
字段
类型
说明
输入
攻防双方数据已存于 baye.data.g_GenAtt[]
ctx.hurt
U8
输出:兵力伤害值
baye.hooks.countAttackHurt = function(ctx) {    var atk = baye.data.g_GenAtt[0];    var def = baye.data.g_GenAtt[1];    // 自定义伤害公式:物攻 - 物防 + 浮动    var base  = atk.PhysAtk - def.PhysDef;    var rand  = 0.85 + Math.random() * 0.3;    var crit  = (Math.random() < atk.crit) ? 1.5 : 1.0;    ctx.hurt = Math.max(1Math.floor(base * rand * crit));    return 0;                  // 接管伤害计算};

5.3 countSkillHurt

内容
调用时机
计算技能对兵力和粮草的伤害时
返回值
0
 = 跳过引擎;-1 = 引擎默认
ctx 参数:
字段
类型
说明
ctx.skillId
U8
技能 ID(输入)
ctx.origin
U8
原始目标对象,AOE 时使用(输入)
ctx.hurt
U8
输出:兵力伤害值
ctx.prov
U8
输出:粮草伤害值
ctx.state
U8
输出:异常状态
baye.hooks.countSkillHurt = function(ctx) {    var atk = baye.data.g_GenAtt[0];    var def = baye.data.g_GenAtt[1];    // 火系技能额外烧粮草    if (lzsg.skillElement(ctx.skillId) == '火') {        ctx.hurt = Math.floor(atk.MagAtk * 1.2 - def.MagDef);        ctx.prov = Math.floor(atk.MagAtk * 0.5);      // 烧粮草        ctx.state = 2;                                 // 燃烧状态        return 0;    }    return -1;                                         // 其他技能走引擎};
✅ 官方示例(exscript/AOE示例.js)— 直线穿透型 AOE
这是官方演示「范围伤害」的范例:技能打中主目标后,处在攻击者与主目标连线上、且离主目标 ≤2 格的单位一并受伤。
// 已知两点求直线方程 ax + by + c = 0function resolveEquationWithPoints(x0, y0, x1, y1) {    return {a: y1 - y0, b: x0 - x1, c: x1*y0 - x0*y1};}// 点到直线的距离function distanceFromPointToLine(x, y, line) {    return Math.abs(line.a*x + line.b*y + line.c) / Math.sqrt(line.a*line.a + line.b*line.b);}baye.hooks.countSkillHurt = function(context) {    var iAim = context.origin;                              // ① 主目标(技能指定的那个)    var iSrc = baye.data.g_GenAtt[0].generalIndex;          // ② 施法者    var iDst = baye.data.g_GenAtt[1].generalIndex;          // ③ 本次正在结算的承受者    var aim = baye.data.g_GenPos[iAim];    var src = baye.data.g_GenPos[iSrc];    var dst = baye.data.g_GenPos[iDst];    var line   = resolveEquationWithPoints(src.x, src.y, aim.x, aim.y);    var d2line = distanceFromPointToLine(dst.x, dst.y, line);      // 承受者到连线的垂距    var d2aim  = Math.sqrt(Math.pow(dst.x-aim.x,2) + Math.pow(dst.y-aim.y,2));    context.hurt = 0;                        // 默认无伤害    if (d2line < 0.5 && d2aim <= 2) {        // 在连线上 且 离主目标 2 格内        context.hurt = 100 - 30*d2aim;       // 距离越远伤害越低    }};

⚠️ 三个"目标"别搞混(这是本钩子最大的坑)

来源
含义
ctx.origin
主目标——玩家用技能时指定的那一格,全程固定
g_GenAtt[0].generalIndex
施法者
g_GenAtt[1].generalIndex
当前正在结算的承受者——本钩子会被逐个单位回调多次

也就是说:一次 AOE 技能,引擎会对每个可能受伤的单位各调一次countSkillHurt每次 ctx.origin 相同(主目标),但 g_GenAtt[1] 是不同的人。想做「只打主目标」就判断 iDst == iAim

📌 ctx.skillId 是 1 起算的,取技能名要 -1baye.getSkillName(ctx.skillId - 1)

📌 想把技能改成「不分敌我」的超级技能,官方示例直接改引擎数据:baye.data.g_Skills[4].aim = 6;   // 火攻(下标4)改为伤害不分敌我


5.4 calcAttackRange

内容
调用时机
战场每次下攻击命令
返回值
无(通过 ctx 输出)
ctx 参数:
字段
类型
说明
ctx.personIndex
U8
将领序号(输入)
ctx.ter
U8
将领所处地形(输入)
ctx.type
U8
攻击类型:0=普攻,1=技能(输入)
ctx.skillId
U8
技能 ID(输入)
ctx.range
[U8]
输出:攻击范围矩阵
ctx.rangeSize
U8
输出:矩阵边长(5x5 就填 5)
baye.hooks.calcAttackRange = function(ctx) {    // 远程兵种普攻范围 3x3    if (ctx.type == 0 && lzsg.isRanged(ctx.personIndex)) {        var size = 3, half = 1;        ctx.range = [];        for (var y = 0; y < size; y++) {            for (var x = 0; x < size; x++) {                var dx = Math.abs(x - half), dy = Math.abs(y - half);                ctx.push((dx + dy) <= half ? 1 : 0);   // 菱形范围            }        }        ctx.rangeSize = size;    }    // 不设置则引擎用默认范围};

5.5 showSkill

内容
调用时机
施展技能时(判定是否成功)
返回值
无(通过 ctx 输出)
ctx 参数:ctx.ter (U8 地形)、ctx.skillId (U8)、ctx.result (U8输出0=失败 1=成功)

⚠️ 旧版字段名是 ctx.success,新版改为 ctx.result

baye.hooks.showSkill = function(ctx) {    // 自定义成功率:基础 80%,受地形与吉运影响    var atk = baye.data.g_GenAtt[0];    var rate = 0.8 - ctx.ter * 0.05 + atk.Luck * 0.02;    ctx.result = (Math.random() < rate) ? 1 : 0;};
✅ 官方示例(exscript/AOE示例.js)— 技能必中
调试 AOE 时最烦技能放空,官方直接把成功率钉死:
baye.hooks.showSkill = function(context) {    context.result = 1;        // 1 = 成功(100% 命中),0 = 失败};

💡 这个钩子只管"是否放出来",不管伤害多少——伤害在 countSkillHurt 里算。调试期两个钩子配合用:showSkill 保证必中 + countSkillHurt 打印坐标,就能稳定复现 AOE 逻辑。


5.6 canUseSkill

内容
调用时机
判断技能目标是否满足条件
来源
examples.js
 官方示例
ctx 参数:ctx.skillIndex、ctx.attackerIndex、ctx.targetIndex、ctx.same(是否同阵营)
baye.hooks.canUseSkill = function(ctx) {    // 禁止对同阵营使用攻击类技能    if (ctx.same && lzsg.isAttackSkill(ctx.skillIndex)) {        return false;    }    return true;};

5.7 getSkillIds

内容
调用时机
获取某将领当前可用技能清单
返回值
无(通过 ctx 输出)
ctx 参数:ctx.generalIndex (U8 战场序号)、ctx.skillIds ([U8]输出技能 ID 数组)
baye.hooks.getSkillIds = function(ctx) {    var pid = baye.data.g_FgtParam.GenArray[ctx.generalIndex] - 1;    var P   = baye.data.g_Persons[pid];    var ids = [];    // 按武将等级解锁技能    if (P.Level >= 5)  { ids.push(2); }    if (P.Level >= 10) { ids.push(3); }    if (P.Level >= 20) { ids.push(4); }    ctx.skillIds = ids;};

5.8 getMaxArms

内容
调用时机
玩家或 AI 执行分配兵力命令前
返回值
无(通过 ctx 输出)
ctx 参数:ctx.personIndex (U8 将领序号)
baye.hooks.getMaxArms = function(ctx) {    var P = baye.data.g_Persons[ctx.personIndex];    // 最大兵力 = 等级 × 100 + 统率 × 50    ctx.maxArms = P.Level * 100 + P.Force * 50;};

6. 战斗流程 — AI 与状态类

6.1 aiFightCommand

内容
调用时机
AI 每次计算将领行军策略前
返回值
无(通过 ctx 输出)
典型用途
完全自定义 AI 行军/战斗决策
ctx 参数:
字段
类型
说明
ctx.sIdx
U8
需要计算行军的将领战场序号(输入)
ctx.type
U8
输出:命令类型 0=普攻,1=技能,3=休息
ctx.param
U8
输出:技能序号(选技能时)
ctx.aIdx
U8
输出:攻击/技能目标的战场序号
baye.hooks.aiFightCommand = function(ctx) {    var me = ctx.sIdx;    // 策略:找血量最低的敌人,能秒就秒,否则休息回血    var target = lzsg.findWeakestEnemy(me);    if (target == baye.None) {        ctx.type = 3;                    // 无目标 → 休息        return;    }    if (lzsg.canKill(me, target)) {        ctx.type  = 0;                   // 普攻        ctx.aIdx  = target;    } else {        ctx.type  = 1;                   // 技能        ctx.param = lzsg.bestSkill(me);        ctx.aIdx  = target;    }};

6.2 battleDrivePersonState

内容
调用时机
驱动战场将领自定义状态
返回值
0
 = 有变化;1 = 无变化
典型用途
实现中毒/燃烧/回复等持续效果
ctx 参数:ctx.generalIndex (U8 战场序号)
baye.hooks.battleDrivePersonState = function(ctx) {    var pos = baye.data.g_GenPos[ctx.generalIndex];    var state = lzsg.getCustomState(ctx.generalIndex);    if (state.poison > 0) {        pos.Arms -= 50;                        // 中毒每回合掉兵        state.poison -= 1;        return 0;                              // 有变化    }    return 1;                                  // 无变化};

6.3 willShowPKAnimation / didShowPKAnimation  ⚠️ 非官方

钩子
时机
用途
willShowPKAnimation
单挑动画播放前
触发攻击方天赋、播放攻击特效
didShowPKAnimation
单挑动画播放后
触发防守方天赋、还原被临时修改的参数

来源:官方文档未记载,baye.wasm 实测存在,LZSG 用于天赋系统。

baye.hooks.willShowPKAnimation = function() {    var atk = baye.data.g_FgtParam.AtkGen;    var def = baye.data.g_FgtParam.DefGen;    // 攻击方天赋:万人莫敌 → 临时加攻    if (lzsg.hasTalent(atk, '万人莫敌')) {        baye.data.g_GenAtt[0].PhysAtk *= 1.5;        lzsg.tempBuffApplied = true;           // 标记,供 did 钩子还原    }};baye.hooks.didShowPKAnimation = function() {    // 还原 will 钩子做的临时修改    if (lzsg.tempBuffApplied) {        baye.data.g_GenAtt[0].PhysAtk /= 1.5;        lzsg.tempBuffApplied = false;    }    // 防守方天赋:铁壁 → 反弹伤害    lzsg.triggerDefenderTalent();};

7. 菜单与 UI 类 ⚠️(多数非官方文档,但 WASM 实测存在)

7.1 willOpenMenu / willCloseMenu / willChangeMenuSelection

这是三个配合使用的菜单钩子,是做 UI 改造最重要的组合。
钩子
时机
ctx
willOpenMenu
菜单即将打开
willChangeMenuSelection
菜单光标移动时
{ index }
willCloseMenu
菜单即将关闭
标准使用模式(LZSG 实战):
// 进入某个菜单时动态挂载,实现”光标跟随显示说明”function showMyMenu() {    var options = ['将领管理''装备管理''库房管理''俘虏管理'];    // ① 挂载光标钩子    baye.hooks.willChangeMenuSelection = function(option) {        lzsg.drawMenuDescription(options[option.index]);   // 右侧显示说明    };    // ② 打开菜单    lzsg.SelMenu1(options, 0function(one) {        if (one == baye.None) {            return;                                        // 取消        }        lzsg.handleMenuChoice(one);    });}// ③ 关闭时清理,避免钩子泄漏到别的菜单baye.hooks.willCloseMenu = function() {    baye.hooks.willOpenMenu = undefined;    baye.hooks.willChangeMenuSelection = undefined;};

⚠️ 必坑点willChangeMenuSelection 是全局单例,用完必须在 willCloseMenu 里置 undefined,否则会污染后续所有菜单。

✅ 官方示例 A(exscript/动态获取菜单选择项.js)— 最简形态
baye.hooks.willChangeMenuSelection = function(c) {    console.log(sprintf(”(%d,%d)-(%d,%d) selection changing to:%d”,        baye.data.c_Sx, baye.data.c_Sy,        baye.data.c_Ex, baye.data.c_Ey,     // 当前菜单的绘制边界        c.index));    baye.drawText(00, ”选中:” + c.index);};baye.hooks.willCloseMenu = function(c) {    console.log(”菜单退出, 删除钩子”);    baye.hooks.willChangeMenuSelection = undefined;      // 用完必须清    baye.hooks.willCloseMenu = undefined;};

📌 baye.data.c_Sx/c_Sy/c_Ex/c_Ey 是当前菜单的绘制边界(左上角/右下角坐标),由引擎在开菜单时填好。想让说明文字画在菜单旁边,用它们算位置最稳。

✅ 官方示例 B(exscript/菜单choose示例.js)— 多级菜单 + 限定绘制区域
官方这套 setDrawBounds / drawText 封装很有用:临时改绘制边界,让文字画在指定矩形内,画完再还原
// 临时设置绘制边界,返回旧值以便还原function setDrawBounds(bounds) {    var old = [baye.data.c_Sx, baye.data.c_Sy, baye.data.c_Ex, baye.data.c_Ey];    baye.data.c_Sx = bounds[0];    baye.data.c_Sy = bounds[1];    baye.data.c_Ex = bounds[2];    baye.data.c_Ey = bounds[3];    return old;}// 在指定矩形内画文字(不传 bounds 则全屏)function drawText(x, y, text, bounds) {    bounds = bounds ? bounds : [00, baye.data.g_screenWidth, baye.data.g_screenHeight];    var old = setDrawBounds(bounds);    baye.drawText(x, y, text);    setDrawBounds(old);                    // 关键:画完还原,否则影响后续所有绘制}baye.hooks.willCloseMenu = function() {    baye.hooks.willOpenMenu = undefined;    baye.hooks.willChangeMenuSelection = undefined;};baye.hooks.showMainHelp = function(context) {    baye.clearRect(33138141);        // 清矩形 (x0,y0,x1,y1)    baye.drawRect(33138141);         // 画边框    drawText(88, ”人物简介”);    var items = ['张三''李四'];    // 第一级菜单:光标移动时在下方显示当前人名    baye.hooks.willChangeMenuSelection = function(c) {        drawText(88+13, items[c.index]);    };    baye.choose(14466060, items, 0function(choice) {        if (choice == baye.None) {            console.log(”操作取消”);        } else {            baye.clearRect(14173210141);            baye.drawRect(14173210141);            drawText(14375, ”指令简介”);            nextLevel(choice);             // 进第二级菜单        }    });    function nextLevel(choice) {        var items = ['招降俘虏''交换俘虏''出售俘虏''流放俘虏''取消操作'];        // 换一个新的光标钩子(覆盖上一级的)        baye.hooks.willChangeMenuSelection = function(c) {            drawText(14375+13, items[c.index]);        };        baye.choose(144216048, items, 0function(c) {            if (c == baye.None) { console.log(”操作取消”); }            else { console.log(c); }        });    }}

💡 baye.choose(x, y, w, h, items, default, cb) — 在任意位置开菜单。与 baye.centerChoose(x, y, items, default, cb)(居中菜单)的区别:choose 多两个尺寸参数,可以精确摆位置。

⚠️ 多级菜单时,每进一级都要重新给 willChangeMenuSelection 赋值——因为钩子是单例,第二级的闭包 items 跟第一级不是同一个数组,不覆盖就会显示错内容或 undefined


⚠️ 无效钩子:didChangeMenuSelection
scripts/ 下有 3 个脚本出现过这个名字:
baye.hooks.didChangeMenuSelection = undefined;      // script (7)/(8)/(10).js 
但它不是一个真实存在的钩子,三条证据:
baye.wasm
(4.4 MB)二进制中找不到该字符串——本次 71 个候选名逐一比对,仅此一个未命中;
官方手册与 js/examples.js 中均无记载;
那 3 处用法全是= undefined的清理动作,没有任何一个是注册实现。
推测:作者本意是清理 willChangeMenuSelection,写错了前缀(did vs will)。引擎既然不会回调它,这行代码就是死代码,删掉即可。
官方的清理写法是这样的(examples.js:389):
baye.hooks.willCloseMenu = function(ctx) {    baye.hooks.willChangeMenuSelection = null;      // 注意是 will 开头,且用 null};

7.2 onMenuIdle

内容
调用时机
菜单空闲时,约每 0.5 秒调用一次
ctx
{ index }
 — 当前选中项
典型用途
菜单动画、光标闪烁、实时预览
baye.hooks.onMenuIdle = function(ctx{    baye.drawText(00, ”selected:” + ctx.index);    // 光标反显闪烁    var x = 8 * ctx.index, y = 20, w = 8, h = 8;    baye.revertRect(x, y, x + w - 1, y + h - 1);};

7.3 showMainHelp

内容
调用时机
玩家打开游戏帮助
参数
典型用途
完全接管帮助界面
baye.hooks.showMainHelp = function() {    // 接管帮助,改成一个字体预览菜单    baye.hooks.willChangeMenuSelection = function(ctx) {        var oldFont = currentFont;        setFont(ctx.index);        baye.clearRect(006030);        baye.drawText(00, ”字体预览”);        setFont(oldFont);    };    baye.centerChoose(2850, [”默认”, ”仿宋”, ”黑体”, ”楷体”, ”宋体”],        currentFont, function(idx) {            if (idx != baye.None) { setFont(idx); }        });};
✅ 官方示例 A(exscript/显示版本示例.js)— 显示版本信息
var libversion = ”1.2.3.4”;var customData = {};// 在主策略界面按[帮助]键或点击右下角触发baye.hooks.showMainHelp = function() {    baye.clearScreen();    var text = ”版本信息\n”;    text += ”引擎    :” + baye.data.g_engineVersion + ”\n”;    text += ”数据    :” + libversion + ”\n”;    text += ”开局引擎:” + customData.engineVersion + ”\n”;   // 这局开局时的引擎版本    text += ”开局数据:” + customData.libversion + ”\n”;    baye.drawText(00, text);};baye.hooks.didOpenNewGame = function() {    customData.libversion   = libversion;                    // 开局时记住版本    customData.engineVersion = baye.data.g_engineVersion;};

💡 同时记录「开局版本」和「当前版本」,是为了识别老存档——对不上就走迁移逻辑(见 1.3)。

✅ 官方示例 B(exscript/enterbattle.js)— 一键直接进战场
调试战斗逻辑时,不用从内政慢慢点进去,官方直接在帮助键上挂一个「立即开打」:
baye.hooks.showMainHelp = function() {    // GenArray: 下标 0~9 我方将领槽位,10~19 敌方;值为 人物ID+1,0 表示空位    baye.data.g_FgtParam.GenArray = [        1000000000,      // 我方:1 号人物        2000000000,      // 敌方:2 号人物    ];    baye.data.g_FgtParam.MapId     = 110 + 1;  // 地图起始 ID 110    baye.data.g_FgtParam.Way       = 1;        // 进攻方向    baye.data.g_FgtParam.CityIndex = 1;        // 城池 ID    baye.data.g_FgtParam.Mode      = 0;        // 1=进攻 / 0=防守    baye.data.g_FgtParam.MProvender = 500;     // 我方粮草    baye.data.g_FgtParam.EProvender = 500;     // 敌方粮草    baye.enterBattle(function() {        console.log(”战斗结束”);               // 战斗结束回调    });};

📌 baye.enterBattle(cb) 是脚本主动进战场的 API(与同名钩子 baye.hooks.enterBattle 是两回事)。先填好 g_FgtParam 再调它即可。

g_FgtParam
 字段
含义
GenArray[20]
参战将领槽位,值 = 人物 ID + 1,0 为空
MapId
战场地图 ID
Way
进攻方向
CityIndex
城池 ID
Mode
1 = 进攻,0 = 防守
MProvender
 / EProvender
我方 / 敌方粮草

7.4 mainSystemMenu

内容
调用时机
主系统菜单(策略结束 / 存盘 / 退出)
返回值
返回值被当作原系统菜单的选择项
baye.hooks.mainSystemMenu = function() {    baye.centerChoose(6030, [”策略结束”, ”存储进度”, ”退出游戏”], 0function(idx) {        return idx;                 // 映射回引擎菜单项    });};

7.5 fightOpenMainMenu

内容
调用时机
战场系统菜单
返回值
原系统菜单的选择项
baye.hooks.fightOpenMainMenu = function() {    baye.centerChoose(6030, [”回合结束”, ”全军撤退”], 0function(idx) {        return idx;    });};

7.6 fightChooseAction

内容
调用时机
战场移动将领后的动作选择菜单
ctx
{ index }
 — 将领序号
返回值
原菜单选择项
baye.hooks.fightChooseAction = function(ctx) {    console.log(”person = ” + ctx.index);    baye.centerChoose(3060, [”攻击”, ”计谋”, ”查看”, ”待机”], 0function(idx) {        return idx;    });};

7.7 fightChooseSkill

内容
调用时机
战场技能选择菜单
ctx
{ index }
 — 将领序号
返回值
技能 ID;baye.None = 取消
baye.hooks.fightChooseSkill = function(ctx) {    var skills = [234];    var names  = skills.map(function(x) { return baye.getSkillName(x); });    baye.centerChoose(3060, names, 0function(idx) {        if (idx == baye.None) { return baye.None; }        return skills[idx];                 // 返回真实技能 ID    });};

7.8 meetFight

内容
调用时机
玩家城池被进攻时选择出战
ctx
{ city }
 — 被进攻城池
返回值
1
 = 出战;0 = 谈判(敌军放回归属地)
baye.hooks.meetFight = function(ctx) {    console.log(”meet fight at ” + ctx.city);    var items = [”出战”, ”谈判”];    baye.centerChoose(4030, items, 0function(index) {        switch (items[index]) {            case ”出战”: return 1;      // 进入选将界面            case ”谈判”: return 0;      // 敌军撤退            default:     return 1;      // 不能取消,默认出战        }    });};

📌 官方示例用的是解构写法 function({city}) {...},ES6 语法;如需兼容老浏览器请改用 function(ctx){ var city = ctx.city; }


7.9 fightStatusBarTouched

内容
调用时机
战场状态栏被点击
ctx
{ x, y }
 — 点击坐标
返回值
0
baye.hooks.fightStatusBarTouched = function(ctx) {    console.log(sprintf(”status bar is touched at (%d,%d)”, ctx.x, ctx.y));    // 把状态栏三个区域映射成按键    if (ctx.x < 30) {        baye.sendKey(baye.VK_EXIT);        // 左区 → 退出键    } else if (ctx.x < 60) {        baye.sendKey(baye.VK_SEARCH);      // 中区 → 查看键    } else {        baye.sendKey(baye.VK_HELP);        // 右区 → 帮助键    }    return 0;};

7.10 fightWillShowHelp ⚠️ 非官方

内容
调用时机
战场帮助界面即将显示
参数
来源
WASM 实测存在,LZSG 标注"待完成 - 界面重构"
baye.hooks.fightWillShowHelp = function() {    // 自定义战场帮助内容    lzsg.drawBattleHelp();};

7.11 willShowCityInfo ⚠️ 未在脚本中使用

内容
调用时机
城池信息界面即将显示(推断)
来源
仅在 baye.wasm 字符串表中发现,官方文档和 LZSG 均未使用,需实测确认

⚠️ 该钩子为邻域扫描发现,行为未经验证,使用前请先打日志确认调用时机。


8. 绘制与字体类

8.1 didShowMainMap

内容
调用时机
主地图绘制完成后
参数
典型用途
在大地图上叠加侧边栏、自定义标记
baye.hooks.didShowMainMap = function() {    // 在大地图右侧绘制自定义侧边栏    lzsg.drawSideBar();};

8.2 showMiniMap

内容
调用时机
显示小地图(进攻路线地图)时
参数
baye.hooks.showMiniMap = function() {    lzsg.drawAttackRouteMap();};

8.3 didShowFightSituation

内容
调用时机
战场形势图显示后
参数
baye.hooks.didShowFightSituation = function() {    lzsg.drawBattleSituationOverlay();};

8.4 didRefreshFightStateBar

内容
调用时机
战场状态栏刷新时
参数
备注
LZSG 在 enterBattle 里也会主动调用它
baye.hooks.didRefreshFightStateBar = function() {    // 重绘状态栏,显示自定义战斗属性    lzsg.drawCustomStateBar();};// 也可主动触发刷新baye.hooks.didRefreshFightStateBar();

8.5 didRefreshFightSituation / didShowFightStateBar ⚠️ 未在脚本中使用

钩子
推断时机
didRefreshFightSituation
战场形势图刷新时
didShowFightStateBar
战场状态栏显示后

⚠️ 两者仅在 baye.wasm 字符串中发现的"近亲"钩子(与 didShowFightSituation/didRefreshFightStateBar 成对),未在官方文档或 LZSG 中出现,需实测确认。


8.6 drawMapUnit

内容
调用时机
战场地形图单元绘制
ctx
{ x, y, flag, tile }
baye.hooks.drawMapUnit = function(ctx) {    var pic = baye.data.g_FightMap[ctx.tile];    var color = baye.data.g_paintColor;    baye.data.g_paintColor = 170;                    // 地图色稍浅    baye.drawImage(ctx.x, ctx.y, baye.data.g_TileId, 0, pic, !ctx.flag);    baye.data.g_paintColor = color;                  // 用完必须还原};

⚠️ baye.data.g_paintColor 是全局状态,改完必须还原


8.7 drawOneGeneral

内容
调用时机
战场人物绘制
ctx
{ frame, index, pic, x, y }
baye.hooks.drawOneGeneral = function(ctx) {    var color = baye.data.g_paintColor;    baye.data.g_paintColor = 200;    baye.drawImage(ctx.x, ctx.y, 40, ctx.pic, 1);    baye.data.g_paintColor = color;};

8.8 fontImageForChar

内容
调用时机
引擎获取字模时(需先开启自定义字体)
ctx
{ code, zmCode }
 — 输入字符码,输出字模数据
典型用途
接入自定义字体(如系统渲染的抗锯齿字体)
function configFont(fontSize, fontName) {    var fontId = { 485241120 }[fontSize];    baye.setFont(fontId);    baye.clearFontCache();    baye.data.g_engineConfig.useCustomFont   = 1;   // 开启中文字体    baye.data.g_engineConfig.useCustomFontEn = 1;   // 开启英文字体    // 挂载取字模钩子    baye.hooks.fontImageForChar = function(ctx) {        ctx.zmCode = getFontImage(ctx.code);    };}
⚠️ 两种模式:你要的是哪一种?
这个钩子有两种完全不同的用法,用错字段不会报错,只表现为字不显示:
模式
输出字段
填什么
适用场景
A. 自定义字体(整套替换)
ctx.zmCode
完整字模像素数据
接入系统渲染的矢量字体
B. 补充字形(缺字补丁)
ctx.index
图片资源里的序号
字库缺几个生僻字,补上即可
模式 B 是 exscript/补充字形示例.js 演示的,也是最省事的方案。
✅ 官方示例(exscript/补充字形示例.js)— 补充生僻字
baye.hooks.fontImageForChar = function(c) {    // index 为 assets/003_MAIN_SPE.assets/1 下的图片序号    var index = {        0x82e00,   // 傕        0x90aa1,   // 惇        0xb5742,   // 祎        0x8faa3,   // 彧        0xe0414,   // 郃        0xab645,   // 玠        0xae8f6,   // 畯        0x85b17,   // 叡    }[c.code];    if (index != undefined) {        c.index = index;        // 只改认识的字,其余交给引擎    }};

🔍 c.code 是 GBK 内码,不是 Unicode!(已实测证实)

把示例里 8 个汉字逐一取 GBK 编码比对,8/8 全部吻合

汉字
Unicode
GBK
示例中的 code
U+5095
0x82E0
0x82e0
 ✅
U+60C7
0x90AA
0x90aa
 ✅
U+794E
0xB574
0xb574
 ✅
U+5F67
0x8FAA
0x8faa
 ✅
U+90C3
0xE041
0xe041
 ✅
U+73A0
0xAB64
0xab64
 ✅
U+756F
0xAE8F
0xae8f
 ✅
U+53E1
0x85B1
0x85b1
 ✅

用 ch.charCodeAt(0) 拿到的是 Unicode,直接当 key 会全部落空想动态生成映射表,得先把字符转成 GBK 再取值。

📌 模式 B 下必须 if (index != undefined) 才赋值——不认识的字要让引擎继续用内置字库,否则整个字库只剩你补的这几个字。


9. 道具类

9.1 giveTool / takeOffTool

钩子
时机
ctx 输出
giveTool
执行赏赐命令,道具即将装到人物时
ctx.result
 0=失败 1=成功
takeOffTool
执行没收命令时
ctx.result
 0=失败 1=成功

📌 官方文档对这两个钩子的参数描述有笔误(写成"地形/技能ID"),实际应为 personIndex / toolIndex

// 赏赐永远成功baye.hooks.giveTool = function(ctx) {    ctx.result = 1;};// 没收:绑定的装备不可没收baye.hooks.takeOffTool = function(ctx) {    var tool = baye.data.g_Tools[ctx.toolIndex];    if (tool.Add.ExtraAttr[0].companionId != 0) {        ctx.result = 0;                     // 已绑定 → 失败    } else {        ctx.result = 1;    }};

9.2 willGiveTool / willTakeOffTool

钩子
时机
返回值
willGiveTool
即将赏赐某道具给某人
0
 = 跳过引擎;-1 = 引擎默认
willTakeOffTool
即将没收某人某道具
0
 = 跳过引擎;-1 = 引擎默认
ctx 参数:ctx.cityIndex、ctx.personIndex、ctx.toolIndex
baye.hooks.willGiveTool = function(ctx) {    var tool = baye.data.g_Tools[ctx.toolIndex];    var name = baye.getToolName(ctx.toolIndex);    // 神兵只赏赐给忠诚度 > 90 的将领    if (lzsg.isLegendary(name)) {        var P = baye.data.g_Persons[ctx.personIndex];        if (P.Devotion < 90) {            baye.showMessage(”忠心不足,无法赐予神兵”);            return 0;                       // 跳过引擎,赏赐失败        }    }    return -1;                              // 交给引擎};

💡 与 giveTool/takeOffTool 的区别:will* 是前置拦截(能阻止动作发生),不带 will 的是结果判定(动作已发生,只改成败)。


10. 列表 UI 自定义类 ⚠️ 全部非官方

来源:官方手册与 examples.js 均无记载,baye.wasm 中确认存在,scripts/ 下 10 个实战脚本全部在用这是做「自定义人物表 / 道具表」的核心入口。

这类钩子两两成对:*Title 填表头,*Value 填单元格。引擎逐列、逐行回调。
⚠️ 前置配置:不设这三项,钩子根本不会被回调
这一节 4 个钩子依赖g_uiCfg的声明——引擎要先知道「有几列、每列多宽」才会逐列回调你。只写钩子不写配置,表现是静默失效(表格还是引擎默认的样子,无任何报错)。
baye.data.g_uiCfg.personPropertiesCount = 13;              // 人物表 列数量baye.data.g_uiCfg.personPropertiesDisplayWitdh = [         // 每列宽度(字符数)    444444444101084,];
配置项
说明
g_uiCfg.personPropertiesCount
人物表列数,必须等于表头数组长度
g_uiCfg.personPropertiesDisplayWitdh
每列宽度数组,长度同上

🐛 引擎拼写错误personPropertiesDisplay**Witdh** —— 少了一个 d(正确英文是 Width)。这是引擎侧的真实字段名,照抄即可,写对了反而不生效三个约束:列数 = 表头数组长度 = 宽度数组长度,三者必须一致。

⚠️ 头号大坑:输出字段名不统一
钩子
输出字段
getPersonPropertyTitle
c.value
getPersonPropertyValue
c.value
getToolPropertyTitle
c.title
 ← 不是 value!
getToolPropertyValue
c.value
只有「道具表头」用c.title,其余三个都用c.value。写错不报错,表现为表头空白。

10.1 getPersonPropertyTitle

内容
调用时机
引擎绘制人物列表表头时,逐列回调
ctx
c.propertyIndex
 — 当前列序号(从 0 开始,对应你定义的表头数组下标)
输出
c.value = <表头文本>
返回值
0
(已填充);不填则引擎用默认表头

10.2 getPersonPropertyValue

内容
调用时机
引擎绘制人物列表单元格时,逐行逐列回调
ctx
c.personIndex
 — 人物全局索引(用于取 g_Persons[i]c.propertyIndex — 列序号
输出
c.value = <单元格内容>
(字符串或数字均可)
返回值
0

10.3 getToolPropertyTitle

内容
调用时机
引擎绘制道具列表表头
ctx
c.propertyIndex
输出
c.title = <表头文本>
 ⚠️
返回值
0

10.4 getToolPropertyValue

内容
调用时机
引擎绘制道具列表单元格
ctx
c.toolIndex
 — 道具索引(用于取 g_Tools[i]c.propertyIndex — 列序号
输出
c.value = <单元格内容>
返回值
0
示例:给人物表加一列「特技」
// 表头数组决定列数;两个钩子的 propertyIndex 都指向这个数组var baye_person_heads = ['姓名''等级''武力''智力''兵力''特 技'];baye.hooks.getPersonPropertyTitle = function(c) {    c.value = baye_person_heads[c.propertyIndex];    return 0;};baye.hooks.getPersonPropertyValue = function(c) {    var person = baye.data.g_Persons[c.personIndex];    switch (baye_person_heads[c.propertyIndex]) {        case '姓名':  c.value = baye.getPersonName(c.personIndex); break;        case '等级':  c.value = person.Level;                     break;        case '武力':  c.value = person.Force;                     break;        case '智力':  c.value = person.IQ;                        break;        case '兵力':  c.value = person.Arms;                      break;        case '特 技': c.value = privateSkillName(c.personIndex);   break;    }    return 0;                       // 忘了 return 0 引擎会忽略本次填充};
✅ 官方示例 A(exscript/扩展属性存档.js)— 加一列「吉运」(最小可运行版)
// ① 先声明列数(1 列)baye.data.g_uiCfg.personPropertiesCount = 1;baye.data.g_uiCfg.personPropertiesDisplayWitdh = [4];// ② 表头数组 —— 两个钩子共用,propertyIndex 就是它的下标var baye_person_heads = [”吉运”];baye.hooks.getPersonPropertyTitle = function(c) {        // 填写人物表头    c.value = baye_person_heads[c.propertyIndex];    return 0;};baye.hooks.getPersonPropertyValue = function(c) {        // 填写人物单元格    var person = baye.data.g_Persons[c.personIndex];    switch (baye_person_heads[c.propertyIndex]) {        case ”吉运”:            c.value = person.ext.lucky;                 // 取自 didOpenNewGame 初始化的数据            break;    }    return 0;};
✅ 官方示例 B(js/demos.js+exscript)— 完整 13 列人物表
官方 demo 用 switch 分发列名,还演示了「查人物所在城池」这类需要遍历的派生字段:
baye.data.g_uiCfg.personPropertiesCount = 13;baye.data.g_uiCfg.personPropertiesDisplayWitdh = [    444444444101084,];var baye_person_heads = [    ”等级”, ”统率”, ”武力”, ”智力”, ”兵种”, ”兵力”, ”忠诚”,    ”体力”, ”经验”, ”主道具”, ”副道具”, ”归属”, ”城池”,];baye.hooks.getPersonPropertyTitle = function(c) {    c.value = baye_person_heads[c.propertyIndex];    return 0;};baye.hooks.getPersonPropertyValue = function(c) {    var person = baye.data.g_Persons[c.personIndex];    // 道具 ID 是 1 起算,0 表示没有    function toolName(id) {        return id == 0 ? '' : baye.getToolName(id - 1);    }    // 归属:0=在野 0xff=俘虏 自己=君主 其余=某君主    function belongName(id) {        if (id == 0)    return '无';        if (id == 0xffreturn '俘虏';        if (id - 1 == c.personIndex) return '君主';        return baye.getPersonName(id - 1);    }    // 查人物所在城池:遍历所有城,看谁的人里有他    function getPersonCityName(pindex) {        var cities = baye.data.g_Cities;        for (var i = 0; i < cities.length; i++) {            for (var j = 0; j < cities[i].Persons.length; j++) {                if (cities[i].Persons[j] == pindex) {                    return baye.getCityName(i);                }            }        }        return '未知';    }    switch (baye_person_heads[c.propertyIndex]) {        case ”等级”:   c.value = person.Level;                      break;        case ”统率”:   c.value = person.Force;                      break;        case ”武力”:   c.value = person.Force;                      break;        case ”智力”:   c.value = person.IQ;                         break;        case ”兵力”:   c.value = person.Arms;                       break;        case ”忠诚”:   c.value = person.Devotion;                   break;        case ”体力”:   c.value = person.HP;                         break;        case ”经验”:   c.value = person.Exp;                        break;        case ”主道具”: c.value = toolName(person.Tool1);            break;        case ”副道具”: c.value = toolName(person.Tool2);            break;        case ”归属”:   c.value = belongName(person.Belong);         break;        case ”城池”:   c.value = getPersonCityName(c.personIndex);  break;    }    return 0;};

💡 两个官方示例的共同点:用 switch (baye_person_heads[c.propertyIndex]) 按列名分发而不是 switch (c.propertyIndex) 按下标分发。这样做的好处是调整列顺序时不用改钩子代码——推荐照抄这个写法。

示例:道具表(注意 c.title)
var baye_tool_heads = ['类型''武力''智力''兵种'];baye.hooks.getToolPropertyTitle = function(c) {    c.title = baye_tool_heads[c.propertyIndex];   // ⚠️ 这里是 title 不是 value    return 0;};baye.hooks.getToolPropertyValue = function(c) {    var tool = baye.data.g_Tools[c.toolIndex];    switch (baye_tool_heads[c.propertyIndex]) {        case '类型': c.value = ['装备''使用'][tool.useflag]; break;        case '武力': c.value = tool.at;                        break;        case '智力': c.value = tool.iq;                        break;        case '兵种': c.value = ['--','禁军','神机','骠骑'][tool.arm]; break;    }    return 0;};

11. 战场地图与信息面板类 ⚠️ 全部非官方

来源:官方手册与 examples.js 均无记载,baye.wasm 中确认存在,scripts/ 中 4~5 个脚本在用。这一组是自定义战场自定义信息面板的入口,威力很大。

11.1 loadFightMap

内容
调用时机
战斗开始、加载战场地图时(早于 enterBattle 的绘制阶段)
参数
无 ctxfunction()
用途
覆盖当前战场的地图尺寸与地形数据 → 实现「自定义战场」
返回值
0
 = 我已提供地图,用我的-1 = 我没提供,引擎用内置地图 ⚠️ 关键

⚠️ 返回值是本钩子的核心契约(官方示例明确演示)。不 return -1 的话,未覆盖到的城池会拿到一份空地图或者上一次的残留数据。官方写法是 switch (城池名)default 分支统一 return -1 兜底。

关键:通过 baye.data.g_FgtParam.CityIndex 判断在哪座城开战,然后覆盖三个全局变量:
变量
含义
baye.data.g_MapWid
地图宽(格)
baye.data.g_MapHgt
地图高(格)
baye.data.g_FightMapData
一维地形数组,长度 = MapWid × MapHgt,取值见地形编号
官方示例(exscript/自定义战场地图.js)— 只改汉中这一张图
baye.hooks.loadFightMap = function() {    var city = baye.data.g_FgtParam.CityIndex;    var name = baye.getCityName(city);    console.log('城池序号:' + city);    console.log('城池名称:' + name);    console.log('进攻方向:' + baye.data.g_FgtParam.Way);    switch (name) {        case '汉中': {            baye.data.g_MapWid = 17;            baye.data.g_MapHgt = 18;            baye.data.g_FightMapData = [ 15111111111111111, 151111111111111111, 113111115111111111, 11112221111111151, 11112221114111111, 11112221111111211, 11112221111511111, 111111111111111111, 11111111111111221, 11115111111122221, 11411111111111211, 11111111115151211, 111111111111111111, 1111511111111,20,16,16,16, 1111111111111,17,20,16,24, 1111222111111,17,17,17,17, 1111222111111,17,23,16,21, 1111222111511,23,16,16,16,            ];            break;        }        default:            return -1;                  // ← 其余城池交给引擎默认地图    }    return 0;                           // ← 我提供了地图};

📌 改完地图记得摆人:官方示例末尾注释提醒——换了地图后将领起始位置常常不对,要在 enterBattle 里手动摆(见 4.1 官方示例)。

🗺️ 地形编号(据官方示例数据推断,待实测确认):1 = 平地(占比最高,作背景);2 = 成片障碍(3×3 连块,疑为山地);3 / 4 / 5 = 散布的特殊地形(零星出现);16~`24` = 城池建筑区(示例里集中在右下角)。

🐛 官方示例的一个小 bug:配套的 logMap() 打印函数把宽高写反了for (row < w) for (col < h),索引却用 row*w + col),非正方形地图会打印错乱。自己写打印时记得用 row*MapWid + col 且外层循环走 MapHgt

⚠️ g_FightMapData 元素个数必须严格等于 MapWid × MapHgt,短了会读到越界数据、长了尾部被忽略。

11.2 drawGenerals

内容
调用时机
战场每帧绘制全部将领时(引擎默认绘制之前)
ctx
c.frame
 — 动画帧序号(0 / 1,用于两帧行走动画切换)
用途
完全接管将领绘制(自定义形象、状态图标、血条等)
返回值
0
 = 我已绘制,引擎不要再画默认图形
✅ 官方示例(exscript/自定义战场将领图.js)
baye.hooks.drawGenerals = function(c) {    // 画出全部屏幕范围内的将领图    var g_GenPos = baye.data.g_GenPos;    // 计算屏幕坐标范围(16 像素一格)    var left   = baye.data.g_MapSX;    var top    = baye.data.g_MapSY;    var right  = Math.floor(left + baye.data.g_screenWidth  / 16 - 1);    var bottom = Math.floor(top  + baye.data.g_screenHeight / 16 - 2);    var frame = c.frame;                       // 0 / 1 两帧切换    for (var i = 0; i < 20; i++) {        var p = g_GenPos[i];        if (p.state == 8continue;            // 死亡, 不画        // 在屏幕外, 不画        if (p.x < left || p.x > right || p.y < top || p.y > bottom) continue;        var point = baye.getFighterXY(i);      // 战场格坐标 -> 屏幕像素坐标        var picind;        if (frame == 0) {            picind = 3;                        // 第 0 帧图片序号        } else {            picind = 7;                        // 第 1 帧图片序号        }        baye.drawImage(point.x, point.y, 50, picind, 1);    }};

📌 baye.drawImage(x, y, 资源组, ?, 图片序号, ?) — 官方示例传的是 (x, y, 5, 0, picind, 1)第 3、4、6 个参数含义未在示例中说明(待实测)。第 5 个 picind 是资源组内的图片序号。

⚠️ 返回值存在分歧(需注意)
来源
写法
官方示例 自定义战场将领图.js
没有 return(函数自然结束,返回 undefined
第三方实战脚本 script (10).js
return 0;
按引擎通用的「0 = 我已处理」约定,推荐补上return 0。官方示例不写,可能是因为该版本引擎只看重绘动作、不看返回值;也可能是示例本身的疏漏。
实际表现判断:如果人物出现重影/默认图形叠在自定义图形上,就是没接管成功,加上 return 0 试试。
实战版(带自定义形象与状态图标)
baye.hooks.drawGenerals = function(c) {    var g_GenPos = baye.data.g_GenPos;    var frame = c.frame;    // 屏幕裁剪范围(16 像素一格)    var left   = baye.data.g_MapSX;    var top    = baye.data.g_MapSY;    var right  = Math.floor(left + baye.data.g_screenWidth  / 16 - 1);    var bottom = Math.floor(top  + baye.data.g_screenHeight / 16 - 2);    for (var i = 0; i < 20; i++) {        var p = g_GenPos[i];        if (p.state == 8) { continue; }                 // 8 = 死亡,不画        if (p.x < left || p.x > right || p.y < top || p.y > bottom) { continue; }        var point = baye.getFighterXY(i);               // 取屏幕像素坐标        var picind = (frame == 0) ? 30 : 31;            // 两帧切换        baye.drawImage(point.x, point.y, 340, picind, 1);    }    return 0;                                            // 关键:接管};

11.3 getTerrainInfo

内容
调用时机
光标停在战场某格、引擎准备显示地形信息面板
ctx
context.ter
 — 地形编号
输出
context.info = <面板文本>
(支持 \n 换行)
返回值
无(靠 context.info 输出)
baye.hooks.getTerrainInfo = function(context) {    var terNames = ['草原','平原','山地','森林','堡垒','城池','火焰','河流'];    context.info =        '【地形】' + terNames[context.ter] + '\n' +        '——————————————————-\n' +        '兵种        攻击系数       防御系数\n' +        '骑兵          120%          105%\n';};

11.4 getFighterInfo

内容
调用时机
光标停在某个武将上、引擎准备显示武将信息面板
ctx
context.index
 — g_GenAtt 索引(战场属性数据)context.generalIndex — 局部 ID(0~19)
输出
context.info = <面板文本>
返回值
baye.hooks.getFighterInfo = function(context) {    var person  = baye.data.g_GenAtt[context.index];            // 战场数据    var pIndex  = baye.data.g_FgtParam.GenArray[context.generalIndex] - 1;  // 全局人物索引    var P       = baye.data.g_Persons[pIndex];    var pos     = baye.data.g_GenPos[context.generalIndex];      // 坐标/生命/状态    context.info = '【' + baye.getPersonName(pIndex) + '】\n' +                   '兵力 ' + P.Arms + '   等级 ' + P.Level + '\n' +                   '状态 ' + ['正常','混乱','禁咒','定身'][pos.state] + '\n' +                   privateSkillInfo(pIndex);};

💡 可在同一钩子里调用 baye.clearRect(...) 自行清屏重绘(实战脚本用来做半透明面板)。


12. 移动与地形计算类 ⚠️ 全部非官方

来源:官方手册与 examples.js 均无记载,baye.wasm 中确认存在,scripts/ 中 4~6 个脚本在用。

12.1 countMove

内容
调用时机
引擎计算某武将本回合移动力
ctx
c.generalIndex
 — 局部 ID(0~19)
输出
直接改 baye.data.g_GenPos[c.generalIndex].move
返回值
不处理时 return -1(交还引擎默认算法);已改写则不要 return -1
baye.hooks.countMove = function(c) {    if (baye.data.g_FgtWeather == 2) {          // 雪天:移动力锁死为 2        baye.data.g_GenPos[c.generalIndex].move = 2;        return;                                  // 已处理,不 return -1    }    return -1;                                   // 其余情况交给引擎};

12.2 countMoveRange ⚠️ 存疑

内容
调用时机
(推测)计算可移动范围时
ctx
context.generalIndex
context.ter(地形编号)
输出
改 baye.data.g_GenPos[context.generalIndex].move
返回值
-1

⚠️ 可信度存疑:10 个实战脚本中,唯一出现此钩子的 script (9).js 把它整段注释掉了(L1538-1549)。说明作者实测后放弃了它。名字确实存在于 baye.wasm,但可能引擎并不真正回调。优先用 countMove 代替。

// 脚本中被注释掉的写法(未验证是否会被回调)// baye.hooks.countMoveRange = function(context) {//     if (person.ArmsType == 3 && context.ter != 7) {//         baye.data.g_GenPos[context.generalIndex].move += 1;//     }//     return -1;// };

12.3 countLandResistance

内容
调用时机
引擎计算某武将的地形阻力表
ctx
context.generalIndex
 — 局部 ID
输出
context.result[i] = <阻力值>
数组,实战脚本填 46 项)
返回值
不处理时 return -1
baye.hooks.countLandResistance = function(context) {    var pIndex = baye.data.g_FgtParam.GenArray[context.generalIndex];    if (baye.getPersonNameByID(pIndex) == '赵云') {        for (var i = 0; i < 46; i++) {            context.result[i] = 0x81;           // 赵云:全地形低阻力        }        return;    }    return -1;                                   // 其他人交还引擎};

⚠️ context.result 是引擎传入的数组,只能逐项赋值,不要整体替换context.result = [...] 会丢失引用,引擎读不到)。


13. 完整钩子速查表

#
钩子名
分类
官方
WASM
有 ctx
1
didOpenNewGame
生命周期
2
willSaveGame
生命周期
3
didLoadGame
生命周期
4
loadPeriod
生命周期
⚠️
5
chooseGameEntry
生命周期
⚠️
6
chooseActor
生命周期
⚠️
7
choosingActorUpdate
生命周期
⚠️
8
tacticStage1
策略阶段
9
tacticStage2
策略阶段
10
tacticStage3
策略阶段
11
tacticStage4
策略阶段
12
tacticStage5
策略阶段
13
tacticStageUser
策略阶段
14
willAddOrder
命令内政
15
willExecuteOrder
命令内政
16
cityMakeCommand
命令内政
17
enterBattle
战斗阶段
18
battleStage1
战斗阶段
19
battleStage2
战斗阶段
20
battleStage3
战斗阶段
21
battleStage4
战斗阶段
22
battleStage5
战斗阶段
23
exitBattle
战斗阶段
24
battleBuildAttackAttriutes
数值计算
25
countAttackHurt
数值计算
26
countSkillHurt
数值计算
27
calcAttackRange
数值计算
28
showSkill
数值计算
29
canUseSkill
数值计算
⚠️
30
getSkillIds
数值计算
31
getMaxArms
数值计算
32
aiFightCommand
AI/状态
33
battleDrivePersonState
AI/状态
34
willShowPKAnimation
AI/状态
35
didShowPKAnimation
AI/状态
36
willOpenMenu
菜单UI
37
willCloseMenu
菜单UI
⚠️
38
willChangeMenuSelection
菜单UI
39
onMenuIdle
菜单UI
⚠️
40
showMainHelp
菜单UI
⚠️
41
mainSystemMenu
菜单UI
⚠️
42
fightOpenMainMenu
菜单UI
⚠️
43
fightChooseAction
菜单UI
⚠️
44
fightChooseSkill
菜单UI
⚠️
45
meetFight
菜单UI
⚠️
46
fightStatusBarTouched
菜单UI
⚠️
47
fightWillShowHelp
菜单UI
48
willShowCityInfo
菜单UI
49
didShowMainMap
绘制字体
50
showMiniMap
绘制字体
51
didShowFightSituation
绘制字体
52
didRefreshFightStateBar
绘制字体
53
didRefreshFightSituation
绘制字体
54
didShowFightStateBar
绘制字体
55
drawMapUnit
绘制字体
⚠️
56
drawOneGeneral
绘制字体
⚠️
57
fontImageForChar
绘制字体
⚠️
58
giveTool
道具
59
takeOffTool
道具
60
willGiveTool
道具
61
willTakeOffTool
道具
62
getPersonPropertyTitle
列表 UI
63
getPersonPropertyValue
列表 UI
64
getToolPropertyTitle
列表 UI
65
getToolPropertyValue
列表 UI
66
loadFightMap
战场地图
67
drawGenerals
战场地图
68
getTerrainInfo
战场地图
69
getFighterInfo
战场地图
70
countMove
移动地形
71
countMoveRange
移动地形
72
countLandResistance
移动地形
didChangeMenuSelection
无效
fightStage1
无效
图例
✅ 官方 = 官方手册有记载(共 31 个)
⚠️ = 官方手册无记载,但 examples.js 或 LZSG 中实际使用(可信)
❌ 官方 = 官方未记载,但 WASM 中存在、且 scripts/ 实战脚本在用(已交叉验证
✅ WASM = 在 baye.wasm 二进制中确认存在 →72 个有效钩子全部命中
❌ WASM = 二进制中找不到该名字 →引擎不会回调,注册了也是死代码
❓ = 参数/时机未确认,需实测

验证口径

  • 第一轮(候选名 71 个):逐一在 4.4 MB 的 baye.wasm 中做字节匹配,仅 didChangeMenuSelection 未命中,其余 70 个全部存在。
  • 第二轮(候选名 21 个,含 exscript 全部钩子):19 个命中,fightStage1 与前缀 fightStage 均未命中(0 次)。

两轮共证伪 2 个无效钩子。详见文末「附:数据来源与验证方法」。


14. 实战避坑清单

说明
解法
拼写
battleBuildAttackAttriutes
 少一个 b
复制本文档的名字,别手敲
钩子泄漏
willChangeMenuSelection
 是全局单例
在 willCloseMenu 里置 undefined
全局状态
baye.data.g_paintColor
 改完不还原会污染后续绘制
try/finally
 或手动还原
存档
customData
 不会被持久化
写回 baye.data.g_Persons[i].Ext.ExtraAttr
返回值
0
 是"我处理了",-1 是"你处理"——含义相反
不确定就 return -1 或干脆不 return
字段名
showSkill
 的 ctx.success 已改名为 ctx.result
用 ctx.result
主动调用
部分钩子可被脚本主动调用做试算
如 battleBuildAttackAttriutes({generalIndex, index})
多人抢钩子
两个 lib 注册同名钩子会互相覆盖
挂载前先备份:var old = baye.hooks.xxx
列表表头字段不统一
getToolPropertyTitle
 用 c.title,另外三个用 c.value
写错不报错,只表现为表头空白
注册了不存在的钩子
didChangeMenuSelection
 在 WASM 中根本不存在
引擎不会回调 → 删掉;真正要的是 willChangeMenuSelection
context.result
 是引用
countLandResistance
 等钩子的 result 由引擎传入
只能 result[i] = x整体赋值会丢失引用
地图数组长度
loadFightMap
 的 g_FightMapData 必须等于 MapWid × MapHgt
短了读到越界数据,长了尾部被截断
loadFightMap
 忘了 return -1
只 return 0 或不 return,未覆盖的城池会拿到空地图
switch
 的 default 分支必须 return -1 兜底
又一个不存在的钩子
fightStage1
 在 WASM 中连前缀都搜不到
正确名字是 battleStage1~`battleStage5`
列表钩子静默失效
没设 g_uiCfg.personPropertiesCount 就不会回调
见第 10 章「前置配置」
引擎字段名拼错
personPropertiesDisplay**Witdh**
(少个 d
这是引擎真实字段名,写对了反而不生效
fontImageForChar
 的 code 是 GBK
charCodeAt(0)
 拿到的是 Unicode,当 key 会全部落空
补字模式要先转 GBK 再取值
读档后 g_Persons 长度不对
残留上一局长度,遍历会碰到野数据
baye.data.g_Persons.length = baye.getPersonCount();
多级菜单钩子未覆盖
第二级菜单沿用第一级的 willChangeMenuSelection 闭包
每进一级都要重新赋值
drawGenerals
 是否接管存疑
官方示例不 return,实战脚本 return 0
出现重影就加 return 0

15. 调试技巧

// 一次性给所有钩子打日志,摸清调用顺序(function() {    var names = ['tacticStage1','tacticStage2','tacticStage3','tacticStage4','tacticStage5',                 'enterBattle','battleStage1','battleStage5','exitBattle',                 'battleBuildAttackAttriutes','countAttackHurt','countSkillHurt'];    names.forEach(function(name) {        var orig = baye.hooks[name];        baye.hooks[name] = function(ctx) {            console.log('[HOOK] ' + name, JSON.stringify(ctx));            return orig ? orig.apply(thisarguments) : -1;        };    });})();
把这段插到脚本最前面,跑一局就能看到所有钩子的触发顺序和 ctx 真实结构。

附:数据来源与验证方法

来源
路径
说明
官方手册
gitee.com/bgwp/baye-doc
_sources/script/index.txt
31 个钩子的权威参数定义
WASM 二进制
js/baye.wasm
(4,422,173 字节)
72 个有效钩子全部命中didChangeMenuSelection 未命中
桥接实现
js/baye.js:1459
js/bridge.js:387
钩子调用机制
官方示例
js/examples.js
约 20 个带注释的可运行示例
官方 demo
js/demos.js
人物表配置、完整 13 列实现
LZSG 实战
_workbench/qmlw-LZSG4.script.js
本 Mod 的钩子使用
第三方实战
_workbench/scripts/*.js
(10 个 Mod,约 1.78 MB)
314 处注册、54 个唯一钩子名
移植作者示例(本次新增)
_workbench/scripts/exscript/
(13 个文件)
18 个钩子的官方权威示例,本版示例主要来源
扫描脚本
_workbench/scan_wasm_hooks.py
从 WASM 提取钩子名的工具
交叉验证脚本
_workbench/verify_hooks.py
三源比对,输出 _verify.txt
exscript 校验脚本(本次新增)
_workbench/verify_exscript.py
钩子存在性 + 编码假设校验
GBK 编码校验脚本(本次新增)
_workbench/verify_gbk.py
从示例脚本提取字符验证 c.code 编码
本次校正(exscript 官方示例交叉比对)
用 _workbench/verify_exscript.py 扫描 13 个示例脚本,得到18 个钩子、覆盖 12 个文件,并校正了以下内容:
类别
校正内容
证伪
fightStage1
 在 WASM 中连前缀 fightStage 都搜不到(0 命中)→ 死代码,正确名是 battleStage1
纠正
loadFightMap
 返回值不是"无明确约定",而是**0=用我的 / -1=引擎默认**,官方 default 分支兜底
补充
fontImageForChar
 有第二种模式c.index(补字)vs c.zmCode(整套字体),原文档只记了后者
证实
c.code
 是 GBK 内码(8/8 字符实测吻合),不是 Unicode
补齐
第 10 章缺失的前置配置g_uiCfg.personPropertiesCount / personPropertiesDisplayWitdh
补齐
读档必须先 baye.data.g_Persons.length = baye.getPersonCount()
补齐
命令 ID 宏定义表(25 个常量,makeCommand 用)
新增
官方线性动画参考实现(引擎无内置动画 API)
记录
drawGenerals
 返回值存在分歧(官方不 return / 实战 return 0)
WASM 存在性复核(2026-09-14 第二轮)
命中: enterBattle, battleStage1, battleStage5, tacticStage1, tacticStage5,      tacticStageUser, drawGenerals, loadFightMap, countSkillHurt, showSkill,      showMainHelp, fontImageForChar, willCloseMenu, willChangeMenuSelection,      didOpenNewGame, willSaveGame, didLoadGame,      getPersonPropertyTitle, getPersonPropertyValue        (共 19 个)未命中: fightStage1, fightStage                              ← 本轮新增证伪
三源数量关系
官方手册记载        31WASM 实测命中       72   ← 有效钩子总数scripts/ 实战使用    54exscript 官方示例    18   ← 权威性最高,本版示例主体→ 72 − 31 = 41 个钩子官方手册从未记载(含新增的 11 个)→ 这就是「官方手册只记了不到一半」的由来
验证方法(2026-09-14 第一轮:scripts/ 第三方 Mod)
WASM 字节匹配—— 71 个候选名逐一 data.find(name):命中 70,仅 didChangeMenuSelection 落空。
脚本侧统计—— 正则 baye\.hooks\.(\w+) 扫 scripts/ 全部 10 个文件:314 处注册、54 个唯一名。
差集比对—— scripts ∪ 文档 与 WASM 求交,定位出 **12 个「实战在用、原文档完全未提及」**的钩子。
语义还原—— 回到脚本读这 12 个钩子的真实实现,反向确定 ctx 字段与返回值约定。
由此发现若干官方永远不会告诉你的细节:
- getToolPropertyTitle 输出用c.title,而其余三个列表钩子用 c.value
- getTerrainInfo / getFighterInfo 用context.info输出面板文本(不是 return)
- countLandResistance 的 context.result 是引擎传入的数组,只能逐项赋值
可信度分级—— 官方 ✅ > WASM 命中且多脚本使用 ⚠️ > WASM 命中但仅单脚本/被注释 ❓ > WASM 未命中 ❌。

⚠️ 免责:第一轮新增的 12 个钩子均无官方文档背书,ctx 字段名是从第三方脚本实现反推的,语义命名(如"地形阻力""移动力")为整理时的推断,实机行为请以调试日志为准。

第二轮已用 exscript 官方示例校正了其中 5 个(getPersonPropertyTitle/ValueloadFightMapdrawGeneralsfontImageForChar),这几条可放心使用。

第二轮补充(exscript 官方示例)
示例脚本扫描—— 13 个文件共 18 个钩子,覆盖率 100%(不含已证伪的 fightStage1)。
编码假设实测—— 从 补充字形示例.js 注释中提取 8 个汉字,逐一取 GBK 编码与 c.code 比对,8/8 吻合,证实 c.code 为 GBK 内码。
第二轮 WASM 复核—— 19 个钩子全部命中,新增证伪 fightStage1(连前缀都搜不到)。

相关学习资料

返回首页浏览学习资料